这是我参与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
其中之一 - 除去用户代理自动设置的首部字段,首部字段仅包括以下几种:
Accept
Accept-Language
Content-Language
Content-Type
:仅限于text/plain
、multipart/form-data
和application/x-www-form-urlencoded
DPR
Downlink
Save-Data
Viewport-Width
Width
- 请求中的任意
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);
}
}