目录
说到跨域问题,首先来说说浏览器的同源策略
1. 同源策略
同源策略(Same Origin Policy)是由 网景通信公司(Netscape) 提出的一个著名的安全策略,它是浏览器最核心也最基本的安全功能,现在所有支持 JavaScript 的浏览器都会使用这个策略。该策略的目的是防止某个文档或者脚本从其他不同的源装载。所谓同源是指协议、域名以及端口要相同。其中有一个条件不满足,则浏览器会认为该资源是从不同源得来的,进而不允许访问。
2. 跨域问题描述
在我们Web开发过程中,经常会遇到跨域问题,就是因为浏览器受到同源策略的限制,对于非同源的资源,Cookie、LocalStorage 和 IndexDB 无法读取; DOM 无法获得;AJAX 请求不能发送。
(图片来源)跨域请求的例子,可以看下表:
解决跨域问题的方案有JSONP和CORS等等。因为JSONP只能发GET请求,不支持其他类型请求,且主要被老的浏览器支持等限制问题。所以推荐使用CORS。
3. CORS概念
CORS(跨域资源共享)(CORS,Cross-origin resource sharing)是一个 W3C 标准,它是一份浏览器技术的规范,它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了Ajax只能同源使用的限制。
CORS需要浏览器和服务器支持,对于开发者来说,浏览器是无需我们额外操作的,因为浏览器一旦发现请求跨域,就会自动添加一些附加的头信息或者多出一次附加请求来进行跨域请求,所以我们只需要专注于如何提供服务器支持。
在Spring Boot中提供了CORS的解决方案,下面来看看在Spring Boot中如何实现CORS。
4. 使用示例
创建两个Spring Boot的项目,两个项目配置不同的端口。都添加spring-boot-starter-web
依赖。
第一个项目相当于服务提供方,配置控制器,如下:
@RestController
public class HelloController {
@GetMapping("hello")
public String hello(){
return "hello get";
}
@PostMapping("hello")
public String hello2(){
return "hello post";
}
}
第二个项目相当于使用服务方,配置端口号为8082,在resourse/static目录下创建一个html文件。内容如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="https://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script>
</head>
<body>
<div id="app1"></div>
<div id="app2"></div>
<input type="button" onclick="btnClick()" value="get按钮">
<input type="button" onclick="btnClick2()" value="post按钮">
<script>
function btnClick() {
$.get('http://localhost:8080/hello', function (msg) {
$("#app1").html(msg);
});
}
function btnClick2() {
$.post('http://localhost:8080/hello', function (msg) {
$("#app2").html(msg);
});
}
</script>
</body>
</html>
两个项目启动,然后在浏览器访问第二个项目的html
点击get按钮后,在控制台发现报错误
这是由于同源策略的限制,导致了请求发送失败。
5.解决方案
5.1 使用@CrossOrigin注解实现细粒度控制
直接在需要被跨域访问的方法上加上@CrossOrigin
注解,注解中配置请求路径就行了。
@RestController
public class HelloController {
@GetMapping("hello")
@CrossOrigin(value="http://localhost:8082")
public String hello(){
return "hello get";
}
@PostMapping("hello")
@CrossOrigin(value="http://localhost:8082")
public String hello2(){
return "hello post";
}
}
@CrossOrigin(value="http://localhost:8082")
表示接口接受来自http://localhost:8082地址的请求
重启两个项目,浏览器访问:
可以看到浏览器的请求网络控制台的响应头中多了以下信息
@CrossOrigin注解的定义如下:
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CrossOrigin {
}
通过元注解@Target的定义可以知道==@CrossOrigin注解可以定义在方法和类上==。定义在类上则整个Controller类中的方法都会控制。
上面请求头信息中,添加了一个Origin
字段,用来说明本次请求的源(协议+域名+端口号)。然后服务器来决定是否同意这个请求。
如果Origin的指定的源不在服务器中配置的许可范围,响应头就没有Access-Control-Allow-Origin
字段。
如果在许可范围内,服务器返回的响应就会多出图中标识的与CORS请求相关的头信息字段。都是以Access-Control-
开头。其中Access-Control-Allow-Origin
,是必须的。它的值要么是请求时Origin
字段的值,要么是一个*
,表示接受任意域名的请求。
5.2 实现WebMvcConfigurer接口全局配置
当多个方法都需要跨域请求时,则可以通过全局配置来解决CORS,创建一个配置类里面重写addCorsMappings
方法即可。
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry corsRegistry){
corsRegistry.addMapping("/**")
.allowedOrigins("http://localhost:8082")
.allowedMethods("*")
.allowedHeaders("*");
}
}
addMapping
表示要添加可以被跨域请求的路径,“/**”
表示本应用的所有方法都可以被跨域请求;allowedOrigins
表示允许访问的url,allowedMethods
表示允许的HTTP方法 ,allowedHeaders
表示允许的请求头。
5.3 WebMvcConfigurerAdapter类全局配置
这个方法也是通过重写addCorsMappings
方法实现的
@Configuration
public class WebMvcConfig {
@Bean
public WebMvcConfigurer corsConfigurer(){
return new WebMvcConfigurerAdapter() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**").allowedOrigins("http://localhost:8082").allowedMethods("*").allowedHeaders("*");
}
};
}
}
WebMvcConfigurerAdapter在Spring 5开始已经被标识为@Deprecated,不推荐使用了,所以还是推荐上面的实现WebMvcConfigurer接口的方法。
5.4 过滤器配置
定义一个过滤器,在响应的时候修改响应头
import org.springframework.context.annotation.Configuration;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebFilter(filterName = "CorsFilter ")
@Configuration
public class CorsFilter 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, PATCH, DELETE, PUT");
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
chain.doFilter(req, res);
}
}
6. 简单请求和非简单请求
上面示例中使用的get
和post
都属于简单请求。简单请求需要符合以下条件:
- 请求方法只能为HEAD、GET、POST
- 请求头中无自定义头,HTTP的头信息不超出以下几种字段: Accept、Accept-Language、Content-Language、Last-Event-ID、Content-Type
- Content-Type必须为text/plain、multipart/form-data、application/x-www-form-urlencoded
不符合以上条件的是非简单请求。对于非简单请求,浏览器会默认发送两条请求,首先第一条为预检请求(OPTION
),浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些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 该字段可选,用来指定本次预检请求的有效期,单位为秒。在有效期间,不用发出另一条预检请求。
担任请求方的项目在页面中增加一个put
请求:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="https://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script>
</head>
<body>
<div id="app1"></div>
<div id="app2"></div>
<input type="button" onclick="btnClick()" value="get按钮">
<input type="button" onclick="btnClick2()" value="post按钮">
<input type="button" onclick="btnClick3()" value="put按钮">
<script>
function btnClick() {
$.get('http://localhost:8080/hello', function (msg) {
$("#app1").html(msg);
});
}
function btnClick2() {
$.post('http://localhost:8080/hello', function (msg) {
$("#app2").html(msg);
});
}
function btnClick3() {
$.ajax({
url:'http://localhost:8080/hello',
type:'put',
success:function (msg) {
$("#app2").html(msg);
}
});
}
</script>
</body>
</html>
担任服务方的项目的控制器增加一个put请求处理方法:
@PutMapping("hello")
public String hello3(){
return "hello put";
}
担任服务方的项目的配置类更改为如下:
这里只添加了maxAge
的配置,目的在于将预检命令进行缓存,不需要每次请求都先执行预检请求,以减少服务器性能消耗。
@Configuration
public class WebMvcConfig implements WebMvcConfigurer{
@Override
public void addCorsMappings(CorsRegistry corsRegistry){
corsRegistry.addMapping("/**")
.allowedOrigins("http://localhost:8082")
.allowedMethods("*")
.allowedHeaders("*")
.maxAge(3600);
}
}
在浏览器中进行测试,可以看到一共发了两次请求:
第一次请求的头信息字段:
可以看到预检请求用的请求方法是OPTIONS
,表示这个请求是用来询问的。
第二次请求的头信息字段:
服务器收到预检请求后,检查了Origin
、Access-Control-Request-Method
和Access-Control-Request-Headers
字段以后,确认允许跨源请求,就可以做出HTTP回应。
HTTP回应中即响应头中Access-Control-Allow-Origin
表示接受请求的域。
如果浏览器否定了预检请求,会返回一个正常的HTTP回应,但是没有任何CORS相关的头信息字段。这时,浏览器就会认定,服务器不同意预检请求,因此触发一个错误,被XMLHttpRequest对象的onerror回调函数捕获。
访问结果如下:
参考:
1. 阮一峰《跨域资源共享 CORS 详解》
2.https://mp.weixin.qq.com/s/83HNkJnKMhVUyL_Ysjp_Zg