了解跨域资源共享CORS与使用Spring MVC处理跨域

简介

CORS(Cross-Origin Resource Sharing 跨源资源共享),当一个资源请求的域或端口与该资源所在服务器的域或端口不同时即为跨域。

例如,在网页https://www.lwhweb.com/2017/11/08/lombok-use/中,图片引用却是另一个域名的资源。

资源跨域

出于安全考虑,浏览器限制从脚本内发起的跨源HTTP请求,例如,XMLHttpRequestFetch API遵循同源策略。 这意味着使用这些API的Web应用程序只能从加载应用程序的同一个域请求HTTP资源,除非使用CORS头文件。

W3C推荐了一种跨域的访问验证的机制,即CORS(Cross-Origin Resource Sharing 跨源资源共享)。CORS允许 Web 应用服务器进行跨域访问控制,从而使跨域数据传输得以安全进行。

CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。

整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。

因此,实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。

流程

浏览器将CORS请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。

若请求满足所有下述条件,则该请求可视为“简单请求”:

  • 使用以下请求方法之一的

    • GET
    • HEAD
    • POST
  • HTTP头信息不超出以下集合的:
    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type(仅限于:text/plain, multipart/form-data, application/x-www-form-urlencoded)

简单请求

浏览器发出CORS请求,在头信息中增加Origin字段,流程如下图:

简单请求流程

以下是使用axios封装的请求函数,以下内容发送测试的请求均使用此函数——fetch:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const fetch = (type = 'GET', url, param = '') => {
return new Promise((resolve, reject) => {
axios({
method: type,
url: url,
changeOrigin: true,
data: JSON.stringify(param)
}).then((response) => {
resolve(response)
}, err => {
reject(err)
}).catch((error) => {
reject(error)
})
})
}

测试发送js:

1
2
3
4
5
fetch('GET', 'http://127.0.0.1:8090/test').then((response) => {
console.log(response.data)
}).catch((err) => {
console.log(err)
})

发送测试请求报文和响应报文:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 请求报文
GET http://127.0.0.1:8090/test HTTP/1.1
Host: 127.0.0.1:8090
Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache
Accept: application/json, text/plain, */*
Origin: http://127.0.0.1:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36
Referer: http://127.0.0.1:8080/
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,zh-TW;q=0.7
// 响应报文
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Max-Age: 3600
Access-Control-Allow-Headers: Authorization, contentType, Content-Type, Accept,Origin,User-Agent
Content-Type: application/json;charset=UTF-8
Date: Mon, 25 Jun 2018 04:09:32 GMT
Content-Length: 51
{"status":true,"code":200,"message":"操作成功"}

请求报文的字段Origin表示该请求源于http://127.0.0.1:8080
响应报文中的字段 Access-Control-Allow-Origin 值为 * ,说明该资源可以被任意外域访问。
如果 Access-Control-Allow-Origin 值不为 *http://127.0.0.1:8080 ,那么浏览器会抛出错误,如下图所示,此时要做的就是跨域处理了。

跨域错误

使用 Origin 和 Access-Control-Allow-Origin 就能完成最简单的访问控制,若要访问该资源,Access-Control-Allow-Origin 应当为 * 或者是 Origin 字段所指明的域名。

非简单请求

非简单请求是那种对服务器有特殊要求的请求,比如请求方法是PUTDELETE,或者Content-Type字段的类型是application/json

非简单请求的CORS请求会先使用OPTIONS方法发起一个预检请求到服务器,以获知服务器是否允许该实际请求。只有收到肯定答复,浏览器才会发出正式的请求,流程如下图:

预检请求流程图

测试发送js:

1
2
3
4
5
fetch('PUT', 'http://127.0.0.1:8090/test').then((response) => {
console.log(response.data)
}).catch((err) => {
console.log(err)
})

浏览器发现这是一个非简单请求,会先发送一个预检请求,请求服务器确认是否允许该请求,以下是请求报文:

1
2
3
4
5
6
7
8
9
10
11
12
OPTIONS http://127.0.0.1:8090/test HTTP/1.1
Host: 127.0.0.1:8090
Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache
Access-Control-Request-Method: PUT
Origin: http://127.0.0.1:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36
Access-Control-Request-Headers: authorization
Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,zh-TW;q=0.7

预检请求用的请求方法是OPTIONS,字段Origin表示请求来自哪个源。

服务器收到预检请求后,验证了OriginAccess-Control-Request-MethodAccess-Control-Request-Headers字段以后,确认允许该跨域请求,响应报文:

1
2
3
4
5
6
7
8
9
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Max-Age: 3600
Access-Control-Allow-Headers: Authorization, contentType, Content-Type, Accept,Origin,User-Agent
Allow: GET, HEAD, POST, PUT, DELETE, TRACE, OPTIONS, PATCH
Content-Length: 0
Date: Mon, 25 Jun 2018 06:50:11 GMT

响应报文中的字段 Access-Control-Allow-Origin 值为 * ,说明该资源可以被任意外域访问。

服务器会有的其他CORS字段如下:

1
2
3
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Max-Age: 3600
Access-Control-Allow-Headers: Authorization, contentType, Content-Type, Accept,Origin,User-Agent

  1. Access-Control-Allow-Methods

这个字段表示服务器所支持的所有跨域请求的方法,这个方法指的是所有支持的方法,而不仅是浏览器请求的那个方法;这里表示服务器支持GET, POST, PUT, DELETE, OPTIONS方法。

  1. Access-Control-Max-Age

这个字段表示本次预检请求的有效期,单位为秒;在这个有效期内,浏览器不会为同一请求再次发送预检请求;

  1. Access-Control-Allow-Headers

这个字段表示服务器支持的所有头信息字段。

预检请求完成后,浏览器就会发送实际的CORS请求,以下是正常请求的请求报文和响应报文:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 请求报文
PUT http://127.0.0.1:8090/test HTTP/1.1
Host: 127.0.0.1:8090
Connection: keep-alive
Content-Length: 2
Pragma: no-cache
Cache-Control: no-cache
Accept: application/json, text/plain, */*
Origin: http://127.0.0.1:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36
Content-Type: application/x-www-form-urlencoded
Referer: http://127.0.0.1:8080/
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,zh-TW;q=0.7
""
// 响应报文
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Max-Age: 3600
Access-Control-Allow-Headers: Authorization, contentType, Content-Type, Accept,Origin,User-Agent
Content-Type: application/json;charset=UTF-8
Date: Mon, 25 Jun 2018 07:15:13 GMT
Content-Length: 51
{"status":true,"code":200,"message":"操作成功"}

实现方法

Spring从4.2版本开始增加了对CORS的支持,以下是Spring MVC中的几种配置方法。

使用@CrossOrigin注解

我们可以在控制器请求方法(即@RequestMapping注解的方法)添加@CrossOrigin注解来启用CORS,默认情况下@CrossOrigin允许在@RequestMapping注解中指定的所有源和HTTP方法:

1
2
3
4
5
6
7
8
9
@RestController
public class TestController {
@RequestMapping("/test")
@CrossOrigin
public JsonResult test() {
return JsonResult.success();
}
}

也可以为整个控制器启用CORS:

1
2
3
4
5
6
7
8
9
@CrossOrigin
@RestController
public class TestController {
@RequestMapping("/test")
public JsonResult test() {
return JsonResult.success();
}
}

跨域请求成功

使用XML配置文件

在spring配置文件中添加以下配置:

1
2
3
4
5
6
7
8
9
10
11
<mvc:cors>
<mvc:mapping path="/api/**"
allowed-origins="*"
allowed-methods="GET,POST,DELETE,PUT"
allow-credentials="true"
allowed-headers="Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With"
max-age="3600"/>
<mvc:mapping path="/resources/**"
allowed-origins="http://www.lwhweb.com" />
</mvc:cors>

基于过滤器的CORS支持

新增Fileter:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class CORSFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
HttpServletResponse response = (HttpServletResponse) servletResponse;
response.setHeader("Access-Control-Allow-Origin","*");
response.setHeader("Access-Control-Allow-Methods","GET, POST, PUT, DELETE, OPTIONS");
response.setHeader("Access-Control-Max-Age","3600");
response.setHeader("Access-Control-Allow-Headers", "Authorization, contentType, Content-Type, Accept,Origin,User-Agent");
chain.doFilter(request, servletResponse);
}
@Override
public void destroy() {
}
}

修改web.xml配置:

1
2
3
4
5
6
7
8
9
<filter>
<filter-name>CORS</filter-name>
<filter-class>com.isp.filter.CORSFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>CORS</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

如果是Spring Boot则可以直接声明过滤器使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Configuration
public class MyConfiguration {
@Bean
public FilterRegistrationBean corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("http://www.lwhweb.com");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
source.registerCorsConfiguration("/**", config);
FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source));
bean.setOrder(0);
return bean;
}
}

参考:

https://spring.io/blog/2015/06/08/cors-support-in-spring-framework

https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS

http://www.ruanyifeng.com/blog/2016/04/cors.html