1.为什么会出现跨域?
当访问资源与目标资源的协议、域名或端口三者其一出现不一致时就会出现跨域问题。而源(origin)由协议、域名、端口3者组成,url由协议、域名、端口和路径组成,如果访问资源url与目标资源url的协议、域名、端口全部相同时,则表示同源,否则就会出现跨域问题。同源策略(Same origin policy)是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,则浏览器的正常功能可能都会受到影响。可以说Web是构建在同源策略基础之上的,浏览器只是针对同源策略的一种实现。简单来说因为浏览器的安全机制,对于访问非同源的资源时就会出现跨域问题。同源策略分为以下两种:
- DOM同源策略:禁止对不同源页面DOM 进行操作。这里主要场景是iframe跨域的情况,不同域名的iframe是限制互相访问的。
- XMLHttpRequest同源策略:禁止使用XHR对象向不同源的服务器地址发起HTTP请求。
假设目标url是https://www.baidu.com/index.html与下面的访问url进行跨域对比:
访问URL | 是否跨域 | 原因 |
---|---|---|
https://www.baidu.com/more/index.html | 否 | 协议、域名、端口都相同 |
https://map.baidu.com/ | 是 | 域名跟目标域名不同 |
http://www.baidu.com/index.html | 是 | 协议不同,这里访问URL协议是http,目标URL协议是https |
https://www.baidu.com:81/index.html | 是 | 端口号不同 |
那么如何解决跨域呢?解决跨域的方式有很多种,前端可以在开发环境中开启本地代理解决跨域,但解决跨域大多数是后端的事情,后端解决跨域最常见的方式是通过nginx进行请求代理转发,下面介绍SpringBoot解决跨域的三种方式。
2.CORS介绍
上面说了因为浏览器的同源策略,访问非同源的资源会造成跨域,而CORS就是解决跨域最常见的一种方式。
CORS是一个W3C标准,全称是”跨域资源共享”(Cross-origin resource sharing)。它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。
CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。
整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。
因此,实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。
浏览器将CORS请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。
只要同时满足以下两大条件,就属于简单请求。
(1).请求方法是以下三种方法之一:
HEAD
GET
POST
(2).HTTP的头信息不超出以下几种字段:
Accept
Accept-Language
Content-Language
Last-Event-ID
Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain
对于简单请求,浏览器直接发出CORS请求。具体来说,就是在头信息之中,增加一个Origin字段。
非简单请求是那种对服务器有特殊要求的请求,比如请求方法是PUT或DELETE,或者Content-Type字段的类型是application/json。
非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为”预检”请求(preflight)。
浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。
详解响应头:
- Access-Control-Allow-Origin 该字段必填。它的值要么是请求时Origin字段的具体值,要么是一个*,表示接受任意域名的请求。
- Access-Control-Allow-Methods 该字段必填。它的值是逗号分隔的一个具体的字符串或者*,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次”预检”请求。
- Access-Control-Expose-Headers 该字段可选。CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。
- Access-Control-Allow-Credentials 该字段可选。它的值是一个布尔值,表示是否允许发送Cookie.默认情况下,不发生Cookie,即:false。对服务器有特殊要求的请求,比如请求方法是PUT或DELETE,或者Content-Type字段的类型是application/json,这个值只能设为true。如果服务器不要浏览器发送Cookie,删除该字段即可。
- Access-Control-Max-Age 该字段可选,用来指定本次预检请求的有效期,单位为秒。在有效期间,不用发出另一条预检请求。
3.SpringBoot解决跨域
3.1 实现WebMvcConfigurer接口重写addCorsMappings方法
此种方式是全局配置的,除了实现WebMvcConfigurer接口外,还有一种基于Spring旧版本的方式,比如实现WebMvcConfigurerAdapter接口重写addCorsMappings方法,但WebMvcConfigurerAdapter接口在Spring已经被标记为Deprecated(已弃用)。推荐使用实现WebMvcConfigurer接口这种方式。 ```java package com.fly.config;
import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { /**
* addMapping:用于配置可以被跨域的路径,可以任意配置,可以具体到直接请求路径。
* /**表示允许所有路径跨域。
*
* allowedMethods:允许所有的请求方法访问该跨域资源服务器,如:POST、GET、PUT、DELETE等。
*
* allowCredentials:是否允许请求携带证书
*
* allowedOrigins:允许所有的请求域名访问的跨域资源,可以固定单条或者多条内容,
* 如:“http://www.aaa.com”,只有该域名可以访问跨域资源。
*
*
* allowedHeaders:允许请求header的信息,*表示允许所有的请求header访问。
* 可以自定义设置任意请求头信息。
*
* maxAge:设置再次发起预检请求的过期时间
*/
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET","POST","HEAD","PUT","DELETE","OPTIONS")
.allowCredentials(true)
.maxAge(3600)
.allowedHeaders("*");
}
}
<a name="l6kMC"></a>
#### 3.2 基于Filter接口解决跨域
```java
package com.fly.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebFilter(filterName = "CorsFilter")
@Configuration
public class CorsConfig implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletResponse response=(HttpServletResponse)res;
response.setHeader("Access-Control-Allow-Origin","*");
response.setHeader("Access-Control-Allow-Credentials","true");
response.setHeader("Access-Control-Allow-Methods","POST,GET,DELETE,PUT");
response.setHeader("Access-Control-Max-Age","3600");
response.setHeader("Access-Control-Allow-Headers","Origin,X-Requested-With,Content-Type,Accept");
/**
* chain表示当前Filter链对象,doFilter方法用于通知Web容器把请求交给Filter链中的下一个 * Filter去处理,如果当前调用此方法的 Filter 对象是Filter 链中的最后一个 Filter,
* 那么将把请求交给目标 Servlet 程序去处理。
*/
chain.doFilter(req, res);
}
}
3.3 基于@CrossOrigin注解解决跨域
在控制器上或控制器的方法加上@CrossOrigin并指定允许跨域的请求即可。@CrossOrigin如果标识在类上,那么它会作用于该类的每一个方法,@CrossOrigin标识在方法上仅会作用于当前方法。使用@CrossOrigin注解时在每个控制器方法都应明确请求类型。
@CrossOrigin用在类上的示例:
package com.fly.controller;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
@CrossOrigin(origins = "http://localhost:3000",allowedHeaders = "*",allowCredentials = "true",
maxAge = 3600,methods = {
RequestMethod.POST,
RequestMethod.DELETE,
RequestMethod.HEAD,
RequestMethod.PUT,
RequestMethod.PATCH})
@RestController
public class UploadController {
// 上传文件存放目录
public final static String UPLOAD_DIR=System.getProperty("user.dir")+"/upload";
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class Result{
private Integer code;
private String msg;
private Object data;
static Result success(String message,Object data){
return new Result(200,message,data);
}
static Result error(String message){
return new Result(201,message,null);
}
}
@PostMapping("/upload/single")
public Object uploadSingleFile(@RequestParam("file") MultipartFile file){
if(file.isEmpty()){
return Result.error("请选择文件");
}
String fileName=file.getOriginalFilename();
File file1=new File(UPLOAD_DIR+"/"+fileName);
try {
// 转移文件到file1进行存储
file.transferTo(file1);
return Result.success("文件上传成功","文件地址:"+file1.getPath());
}catch (Exception e){
e.printStackTrace();
}
return Result.error("文件上传失败");
}
}
@CrossOrigin用在方法上的示例:
public class GoodsController{
@CrossOrigin(origins = "http://localhost:8080")
@RequestMapping(method =RequestMethod.GET,value="/queryGoodsList")
public Response queryGoodsList(){
return null;
}
}
常用第一、二种方式,第三方粒度更加细,一般不用。