CORS 是一个 W3C 标准,全称叫做"跨域资源共享"(Cross-Origin resource sharing); 在详细介绍 CORS 之前先简单介绍下什么是同源政策,这样才能了解到 CORS 的由来|必要性。
浏览器同源政策
"同源政策"是浏览器安全的基石,目前所有浏览器都实行这个政策。
含义
所谓"同源",是指以下三个相同:
- 协议相同
- 域名相同
- 端口相同
举个例子:
当前网址 | 被请求页面地址 | 是否跨域(不同源) | 原因 |
---|---|---|---|
http://www.wu-yikun.top/page.html | http://www.wu-yikun.top/main.html | 否 | 同协议(http )、同域名(www.wu-yikun.top )、同端口(80 ) |
http://www.wu-yikun.top/page.html | https://www.wu-yikun.top/other.html | 是 | 协议不同(http 与 https ) |
http://www.wu-yikun.top/page.html | http://www.wu-yikun.com/page.html | 是 | 域名不同(www.wu-yikun.top 与 www.wu-yikun.com ) |
http://www.wu-yikun.top/page.html | http://www.wu-yikun.top:8090/other.html | 是 | 端口不同(80 与 8090) |
现在再来看这副图,是不是一目了然了?
目的
节选自"阮一峰"老师的文章
同源政策的目的,是为了保证用户信息的安全,防止恶意的网站窃取数据。
设想这样一种情况:A 网站是一家银行,用户登录以后,又去浏览其他网站。如果其他网站可以读取 A 网站的 Cookie,会发生什么?
很显然,如果 Cookie 包含隐私(比如存款总额),这些信息就会泄漏。更可怕的是,Cookie 往往用来保存用户的登录状态,如果用户没有退出登录,其他网站就可以冒充用户,为所欲为。因为浏览器同时还规定,提交表单不受同源政策的限制。
由此可见,"同源政策"是必需的,否则 Cookie 可以共享,互联网就毫无安全可言了。
限制
随着互联网的发展,同源政策越来越严格。如果非同源,以下几种行为会受到限制。
Cookie
、LocalStorage
和IndexDB
无法读取DOM
无法获取AJAX
请求无法发送
光是一个 AJAX 请求无法发送就可以遏制不少合理的用途了。
接下来介绍如何实现跨域资源共享以避免同源政策。
CORS 详解
简介
CORS 需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,在 CORS 通信过程中,浏览器会自动完成,无需用户参与,所以要想实现 CORS 通信,关键是使得服务器支持 CORS 接口,便可跨源通信。
CORS 两类请求
浏览器将 CORS 请求分为两类:简单请求和非简单请求。
简单请求
只要同时满足以下两大条件,就属于简单请求:
- 请求方法是以下三种类型之一
HEAD
GET
POST
HTTP
头信息不超出以下几种字段Accept
Accept-Language
Content-Language
Content-Type
: 只限于application/x-www-form-urlencoded
、multipart/form-data
、text/plain
三者之一
凡是不同时满足以上两个条件的就属于非简单请求。
如下就是一个 CORS 简单请求:
以下是浏览器发送给服务器的请求报文:
GET /resources/public-data/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Connection: keep-alive
Origin: https://foo.example
请求首部字段 Origin
表明该请求来源于 http://foo.example
,再看看响应报文:
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2021 00:23:53 GMT
Server: Apache/2
Access-Control-Allow-Origin: *
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Transfer-Encoding: chunked
Content-Type: application/xml
[XML Data]
本例中,服务端返回的 Access-Control-Allow-Origin: *
表明该资源可以被任意外域访问。
Access-Control-Allow-Origin: *
使用 Origin
和 Access-Control-Allow-Origin
就能完成最简单的访问控制。如果服务端仅允许来自 https://foo.example
的访问,该首部字段的内容如下:
Access-Control-Allow-Origin: https://foo.example
非简单请求
非简单请求是那种对服务器有特殊要求的请求,比如请求方法是 PUT
或 DELETE
,或者 Content-Type
字段类型是 application/json
。
非简单请求的 CORS 请求的一大特点,就是会在正式通信前增加一次 HTTP 查询请求,称为"预检"请求(Preflight request)。该"预检"请求的方法为 OPTIONS
,"预检"请求的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响。
如下是一个需要执行预检请求的 HTTP 请求:
如下所述,实际的
POST
请求不会携带Access-Control-Request-*
首部,它们仅仅适用于OPTIONS
请求。
下面是服务端和客户端完整的信息交互。
首先是预检请求:
OPTIONS /doc HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Connection: keep-alive
Origin: https://foo.example
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER, Content-Type
浏览器检测到,从 JavaScript 中发起的请求需要被预检。从上面的报文中,我们看到,第 1~10 行发送了一个使用 OPTIONS
方法 的“预检请求”。OPTIONS
是 HTTP/1.1 协议中定义的方法,用以从服务器获取更多信息。该方法不会对服务器资源产生影响。 预检请求中同时携带了下面两个首部字段:
- Access-Control-Request-Method: POST
- Access-Control-Request-Headers: X-PINGOTHER, Content-Type
首部字段 Access-Control-Request-Method
告知服务器,实际请求将使用 POST
方法。
首部字段 Access-Control-Request-Headers
告知服务器,实际请求将携带两个自定义请求首部字段:X-PINGOTHER
与 Content-Type
。
服务器据此决定,该实际请求是否被允许。
预检响应:
HTTP/1.1 204 No Content
Date: Mon, 01 Dec 2021 01:15:39 GMT
Server: Apache/2
Access-Control-Allow-Origin: https://foo.example
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
Access-Control-Max-Age: 86400
Vary: Accept-Encoding, Origin
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
(1)服务器的响应携带了 Access-Control-Allow-Origin: https://foo.example
,从而限制请求的源域。
(2)同时,携带的 Access-Control-Allow-Methods
表明服务器允许客户端使用 POST
和 GET
方法发起请求(与 Allow
响应首部类似,但其具有严格的访问控制)。
(3)首部字段 Access-Control-Allow-Headers
表明服务器允许请求中携带字段 X-PINGOTHER
与 Content-Type
。如果浏览器请求包括Access-Control-Request-Headers
字段,则Access-Control-Allow-Headers
字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在"预检"中请求的字段。
(4)最后,首部字段 Access-Control-Max-Age
表明该响应的有效时间为 86400 秒,也就是 24 小时。在有效时间内,浏览器无须为同一请求再次发起预检请求。请注意,浏览器自身维护了一个 最大有效时间,如果该首部字段的值超过了最大有效时间,将不会生效。
预检请求完成之后,发送实际请求:
POST /doc HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Connection: keep-alive
X-PINGOTHER: pingpong
Content-Type: text/xml; charset=UTF-8
Referer: https://foo.example/examples/preflightInvocation.html
Content-Length: 55
Origin: https://foo.example
Pragma: no-cache
Cache-Control: no-cache
实际响应报文:
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2021 01:15:40 GMT
Server: Apache/2
Access-Control-Allow-Origin: https://foo.example
Vary: Accept-Encoding, Origin
Content-Encoding: gzip
Content-Length: 235
Keep-Alive: timeout=2, max=99
Connection: Keep-Alive
Content-Type: text/plain
上面头信息中,Access-Control-Allow-Origin
字段是每次响应都必须包含的。
Spring Boot 解决跨域问题
讲了这么多,来点实际场景感受一下。
是不是觉得有种某名的"亲切感"?以下我们就来解决这种无力感吧。
通过 Filter 过滤器手动设置响应头
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
@Slf4j
@WebFilter(urlPatterns = {"/*"}, filterName = "corsFilter")
public class CorsFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("启动跨域过滤器");
}
@Override
public void doFilter(ServletRequest request, ServletResponse resp, FilterChain chain) throws IOException, ServletException {
HttpServletResponse response = (HttpServletResponse) resp;
// 手动设置响应头解决跨域访问
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Methods", "POST, PUT, GET, OPTIONS, DELETE");
// 设置过期时间
response.setHeader("Access-Control-Max-Age", "86400");
response.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization, uuid");
// 支持 HTTP 1.1
response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
// 支持 HTTP 1.0. response.setHeader("Expires", "0");
response.setHeader("Pragma", "no-cache");
// 编码
response.setCharacterEncoding("UTF-8");
chain.doFilter(request, resp);
}
@Override
public void destroy() {
log.info("销毁跨域过滤器");
}
}
使用 @CrossOrigin 注解(局部跨域)
@CrossOrigin
注解源码:
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CrossOrigin {
@AliasFor("origins")
String[] value() default {};
@AliasFor("value")
String[] origins() default {};
String[] allowedHeaders() default {};
String[] exposedHeaders() default {};
RequestMethod[] methods() default {};
String allowCredentials() default "";
long maxAge() default -1L;
}
使用 @CrossOrigin
注解:
@CrossOrigin(origins = "*", allowedHeaders = "*", maxAge = 86400)
@PostMapping("/login")
public String login(@RequestBody User user) {
TODO..
}
不过通过 @CrossOrigin
注解的源代码注定了它只能针对单个接口进行跨域配置,即局部跨域。虽然它比如上的 Filter 过滤器更简便,但这明显不是我们想要的,实际开发中也很少使用该注解。
实现 WebMvcConfigurer 接口
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 WebMvcConfiguration implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
// 表明允许哪些域访问, 简单点可为 *
.allowedOrigins("http://localhost:3000")
.allowedHeaders("*")
.allowedMethods("*")
// allowCredentials(true): 表示附带身份凭证
// 一旦使用 allowCredentials(true) 方法,则 allowedOrigins("*") 需要指明特定的域,而不能是 *
.allowCredentials(true)
.maxAge(86400);
}
}
以上这种方式在没有定义拦截器(Interceptor)的时候,使用起来一切正常,但如果你有一个全局的拦截器,比如检测用户登录的拦截器:
public class AuthenticationInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws Exception {
// 从 http 请求头中取出 token
String token = httpServletRequest.getHeader("token");
// 检查是否登录
if (token == null) {
throw new InvalidTokenException(ResultCode.INVALID_TOKEN.getCode(), "登录信息已过期,请重新登录");
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}
当自定义拦截器返回 true
时,一切正常,但是当拦截器抛出异常(或者返回 false
)时,后续的 CORS 配置将不会生效。
为什么拦截器抛出异常 CORS 不生效呢?可以看看 GitHub 上提出的这个 issue:
when interceptor preHandler throw exception, the cors is broken #9595
大致内容如下:
有人提交了一个 issue,说明如果在自定义拦截器的 preHandler
方法中抛出异常的话,通过 CorsRegistry
设置的全局 CORS 配置就失效了,但是 Spring Boot 的成员不认为这是一个 BUG🐛。
然后提交者举了个具体的例子:
他先定义了 CorsRegistry
,并添加了一个自定义的拦截器,拦截器中抛出异常:
然后发现 AbstractHandlerMapping
在添加 CorsInterceptor
的时候,是将 Cors 的拦截器加在拦截器链的最后:
那就会造成上面所说的问题:在自定义拦截器中抛出异常之后,CorsInterceptor
拦截器就没有机会执行向 response
中设置 CORS 相关响应头。
issuer 也给出了解决的方案,就是将用来处理 CORS 的拦截器 CorsInterceptor
夹在拦截器链的第一个位置:
这样的话,一旦请求来了之后,第一个拦截器就会为 response 设置相应的 CORS 响应头(例如: Access-Control-Allow-Origin
、Access-Control-Allow-Methods
、Access-Control-Allow-Headers
),后续如果其他自定义拦截器抛出异常,也不会有任何影响了!
感觉这是一个可行的解决方案,但是 Spring Boot 的成员认为这并不是 Spring Boot 的 Bug,而是 Spring Framework 的 Bug,所有将这个 issue 关闭了。
注入 CorsFilter 过滤器
使用过滤器就可以避免拦截器与全局跨域配置冲突问题。代码如下:
@Configuration
public class CorsFilterConfiguration {
@Bean
public CorsFilter corsFilter() {
// 创建 CorsConfiguration 对象后添加配置
CorsConfiguration corsConfiguration = new CorsConfiguration();
// 设置放行哪些原始域
corsConfiguration.addAllowedOrigin("*");
// 放行哪些原始请求头部信息
corsConfiguration.addAllowedHeader("*");
// 放行哪些请求方法
corsConfiguration.addAllowedMethod("*");
// 添加映射路径
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", corsConfiguration);
return new CorsFilter(source);
}
}
⭐为什么过滤器可以避免冲突而拦截器不行呢?
因为过滤器依赖于 Servlet 容器,基于函数回调,它可以对几乎所有请求进行过滤。而拦截器是依赖于 Web 框架(如 Spring MVC 框架),基于反射通过 AOP 的方式实现的。
触发顺序如下所示:
因为过滤器在触发上是先于拦截器的,但是如果有多个过滤器的话,也需要将 CorsFilter
设置为第一个过滤器才行。
解决 CORS 跨域的其他角度|思路
服务端支持 CORS
🤨你在期待什么?
刚刚所讲的 [Spring Boot 解决跨域问题](#Spring Boot 解决跨域问题) 就是在阐述如何在服务端实现 CORS 跨域资源共享。
以上四种方法亲测有效!如有疑问,评论区见。
😉若你使用 Spring 框架而非 Spring Boot,我也找到了一篇官方文档供你们参考:CORS support in Spring Framework
JSONP
利用 <script>
标签没有跨域限制的漏洞,网页跨域得到从其他来源动态产生的 JSON 数据。JSONP 请求一定需要对方的服务器做支持才可以。
注:有三个标签本身就允许跨域加载资源。
<img src="xxx">
<link href="xxx">
<script src="xxx">
JSONP 与 AJAX 相同,都是客户端向服务器发送请求,从服务器端获取数据的方式。但 AJAX 属于同源策略,JSONP 属于非同源策略(支持跨域请求)。
优点:简单、兼容性好,可用于解决主流浏览器的跨域数据访问的问题。
缺点:仅支持 GET
方法,具有局限性,不安全可能会遭受 XSS 攻击。
反向代理服务器
同源策略是浏览器需要遵守的标准,而如果是服务器向服务器请求则无需遵循同源策略了。所以通过反向代理服务器就可以有效地解决跨域问题了,代理服务器需要做如下几件事:
- 接受客户端请求
- 将请求转发给实际的服务器
- 将服务器的响应结果返回给客户端
Nginx 就是类似的反向代理服务器,跨域通过配置 Nginx 代理来解决跨域问题。
Reference
- developer.mozilla.org/zh-CN/docs/…
- www.cnblogs.com/lenve/p/105…
- www.ruanyifeng.com/blog/2016/0…
- juejin.cn/post/700057…
/ END / 不要以你现在的能力,束缚对未来的想象!