这是我参与8月更文挑战的第5天,活动详情查看:8月更文挑战
开篇注:“跨域”确切来讲应该说成“跨源”,对应于英文的
cross-origin,但是前者的使用频次明显更高,考虑到语言的约定俗称性,可以认为“跨域”和“跨源”两个词是可以等价替换的,所以本文将不加区分地使用两者。
同源策略
浏览器为了保证用户信息安全,应用了一系列安全策略,其中之一就是同源策略 same-origin policy,缩写是 SOP。
所谓同源,指的是以下三者相同:
- 协议相同
- 域名相同
- 端口号相同
同源策略用于约束跨域交互,跨域交互通常分为三类:
- 跨域写
cross-origin writes:比如链接<a>、重定向以及表单提交。 - 跨域资源嵌入
cross-origin embedding:比如使用<script>嵌入脚本,使用<img>标签展示图片,使用<link rel="stylesheet">引入CSS样式等等。 - 跨域读
cross-origin reads:比如XMLHttpRequest请求、读取DOM对象、读取JS对象等等。
并非所有的跨域交互都被禁止:
- 一般允许跨域写,特定少数的
HTTP请求需要添加preflight。 - 一般允许跨域资源嵌入。
- 一般禁止跨域读。
跨域问题
跨域问题是指在同源策略的约束下,某些跨域交互受到限制而引起的问题。比如在接口联调时,前端发送跨域 AJAX 请求,如果没有处理跨域问题,就会在浏览器控制台报错:
Access to XMLHttpRequest at 'aaa' from origin 'bbb' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
总体来说,同源策略限制了以下三种跨域行为,它们都属于跨域读操作:
Cookie、LocalStorage和IndexedDB无法读取。DOM无法获得。禁止对非同源页面的DOM进行操作,比如不同域名的<iframe>不能互相访问。AJAX请求(XMLHttpRequest)不能发送。
CORS 跨源资源共享
CORS 简介
CORS 是 cross-origin resource sharing 的缩写,中文是跨源资源共享,也常被称作跨域资源共享。它是一种基于 HTTP 首部字段的机制,从服务器端(后端)解决了跨域问题。
简单请求与预检请求
浏览器把请求分为两类,简单请求 simple requests 和预检请求 preflighted requests,对于这两类请求的处理流程是不一样的。
简单请求指的是满足以下所有条件的请求:
- 请求方法是
GET、POST、HEAD其中之一 - 除去用户代理自动设置的首部字段,首部字段仅包括以下几种:
AcceptAccept-LanguageContent-LanguageContent-Type:仅限于text/plain、multipart/form-data和application/x-www-form-urlencodedDPRDownlinkSave-DataViewport-WidthWidth
- 请求中的任意
XMLHttpRequestUpload对象均没有注册任何事件监听器。 - 请求中没有使用
ReadableStream对象。
所有不满足上述条件的请求就是预检请求。简单请求和预检请求的主要区别在于简单请求没有预检这一步骤,所以简单请求也可以理解为是不会触发 CORS 预检请求的请求。
简单请求的工作流程:
- 浏览器判定某次请求为简单请求,自动在首部字段中添加
Origin字段,然后向服务器发送请求。 - 服务器查看
Origin中的源是否被允许,如果允许,则返回带有Access-Control-Allow-Origin首部字段的响应。如果不允许,则返回一般响应,该响应没有Access-Control-Allow-Origin。Access-Control-Allow-Origin的值一般为*或者请求中Origin指定的域。 - 浏览器查看响应,如果存在
Access-Control-Allow-Origin首部字段,则正常处理响应,如果没有该字段,说明请求违反同源策略,抛出异常。
预检请求的工作流程:
- 在正式请求服务器之前,浏览器会发出一次预检请求,请求方法为
OPTIONS。其中需要包含Origin首部字段,以指定请求来自哪个源,除此之外还要包括Access-Control-Request-Method和Access-Control-Request-Headers这两个首部字段。 - 服务器收到请求,如果允许该跨域访问,返回带有
Access-Control-Allow-Origin首部字段的响应,需要指定为*或者请求中Origin字段的值。 - 如果服务器不允许该跨域请求,那么返回的响应不带有
Access-Control-Allow-Origin,浏览器将抛出异常。 - 预检请求成功之后开始发送实际请求,工作流程类似于简单请求,浏览器发送的请求需要携带
Origin首部字段,而服务器返回的响应需要携带Access-Control-Allow-Origin字段。 - 对于预检请求,不用每次都先发一次
OPTION请求,在一定的有效时间之内,可以不用重复发送。在服务器响应OPTION预检请求时,可以通过Access-Control-Max-Age来设置该响应的有效时间。请注意,浏览器自身维护了一个最大有效时间,如果该首部字段的值超过了最大有效时间,将不会生效。
CORS 请求首部字段
Origin:指定预检请求或者实际请求的源。Origin参数的值为源站URI。它不包含任何路径信息,只是服务器名称。在所有访问控制请求(Access control request)中,Origin首部字段总是被发送。Access-Control-Request-Method:预检请求专用,用于将实际请求所使用的HTTP方法告诉服务器。Access-Control-Request-Headers:预检请求专用,将实际请求所携带的首部字段告诉服务器。多个字段用逗号分开。
CORS 响应首部字段
Access-Control-Allow-Origin:指定了允许访问该资源的URI。对于不需要携带身份凭证的请求,服务器可以指定该字段的值为通配符,表示允许来自所有域的请求。如果服务端指定了具体的域名而非*,那么响应首部中的Vary字段的值必须包含Origin。这将告诉客户端:服务器对不同的源站返回不同的内容。Access-Control-Expose-Headers:在跨源访问时,XMLHttpRequest对象的getResponseHeader()方法只能拿到一些最基本的响应头,Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma,如果要访问其他头,则需要服务器设置本响应头。比如Access-Control-Expose-Headers: X-My-Custom-Header, X-Another-Custom-Header。Access-Control-Max-Age:设置了预检请求的结果能够被缓存多久,单位为秒。Access-Control-Allow-Credentials:为true时,表示服务器允许浏览器发送cookie。该值只能为true,如果服务器不需要浏览器发送cookie,直接不写该字段即可。如果响应携带了该字段,Access-Control-Allow-Origin不能为*,必须明确指定为请求发送方的域名。Access-Control-Allow-Methods:用于响应预检请求,规定实际请求所允许使用的HTTP方法。Access-Control-Allow-Headers:用于响应预检请求,指明了实际请求中允许携带的首部字段。
Spring Boot 解决跨域问题
在 Spring Boot 中,有多种方案可以解决跨域问题,这里提到的四种方案,按照作用范围可以分为两类:
- 局部跨域
- 全局跨域
局部跨域
- 使用
@CrossOrigin注解
- 应用在
Controller类上面,可以允许该类下面的所有接口接收跨域请求
@RestController
@CrossOrigin
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "hello";
}
}
- 应用在
Controller类的某个方法上,可以允许该方法接收跨域请求
@RestController
public class HelloController {
@GetMapping("/hello")
@CrossOrigin
public String hello() {
return "hello";
}
}
- 使用
HttpServletResponse,手动为响应头添加字段Access-Controll-Allow-Origin
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello(HttpServletResponse response) {
response.addHeader("Access-Control-Allow-Origin", "http://localhost:8080");
return "hello";
}
}
全局跨域
- 定义
CorsConfig.java配置类,实现WebMvcConfigurer接口,重写addCorsMappings方法。
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:3000")
.allowedHeaders("*")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowCredentials(true)
.maxAge(86400);
}
}
- 使用过滤器
@Configuration
public class WebConfig {
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedOrigin("https://localhost:3000");
config.setAllowCredentials(true);
config.addAllowedMethod(CorsConfiguration.ALL);
config.addAllowedHeader(CorsConfiguration.ALL);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
}