文章目录
- 一、跨域介绍
- 1、什么是跨域
- 2、为什么会产生跨域
- 3、禁止跨域的原因
- 二、简单请求和非简单请求
- 1、简单请求
- 1.1、什么时简单请求
- 1.2、简单请求基础流程
- 2、非简单请求
- 2.1、预检请求
- 2.2、预检请求的回应
- 2.3、浏览器的正常请求和回应
- 3、自定义跨域过滤器
- 三、解决方式
- 1、@CrossOrigin注解
- 2、CorsRegistry方式
- 3、CorsFilter过滤器
- 3.1、源码解析
- 4、CorsWebFilter网关WebFlux过滤器
一、跨域介绍
1、什么是跨域
- 同源策略(Sameoriginpolicy)是浏览器最核心也最基本的安全功能
- 一个页面发起的ajax请求,只能是与当前页域名相同的路径,这能有效的阻止跨站攻击
- 所谓同源(即指在同一个域)就是两个页面具有相同的
协议(protocol),主机(host)和端口号
2、为什么会产生跨域
- 前后端分离模式下,客户端请求前端服务器获取视图资源
- 客户端向后端服务器获取数据资源
前端
服务器的协议,IP和端口和后端
服务器不一样
,就产生了跨域
3、禁止跨域的原因
二、简单请求和非简单请求
CORS
是一个W3C标准,全称是"跨域资源共享"(Cross-origin resource sharing)- 它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而
克服了AJAX只能同源使用的限制
- 浏览器将CORS请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)
1、简单请求
1.1、什么时简单请求
只要同时满足以下两大条件,就属于简单请求
- 请求方法是以下三种方法之一
- HEAD
GET
POST
- HTTP的头信息不超出以下几种字段
- Accept
- Accept-Language
- Content-Language
- Last-Event-ID
Content-Type
:只限制三个值- application/x-www-form-urlencoded
- multipart/form-data
- text/plain
1.2、简单请求基础流程
- 浏览器发现这次跨源AJAX请求是简单请求,就自动在头信息之中,添加一个
Origin
字段 - Origin字段用来说明,本次请求来自哪个源(
协议 + 域名 + 端口
)。服务器根据这个值,决定是否同意这次请求
举例
- HTTP请求的方法是
GET
,响应response添加自定义头信息X-Name
- 下面是这个请求的HTTP头信息
- 如果Origin指定的源,
不在许可范围内
,服务器会返回一个正常的HTTP回应- 浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin字段(详见下文)
- 从而抛出一个错误,但是这种错误无法通过状态码识别,因为HTTP回应的状态码有可能是200
- 如果Origin指定的域名
在许可范围内
,服务器返回的响应,会多出几个头信息字段- 有四个与CORS请求相关的字段,都以
Access-Control-
开头
- 有四个与CORS请求相关的字段,都以
- Access-Control-Allow-Origin
- 该字段是必须的
- 要么是请求时Origin字段的值
- 要么是一个*,表示接受任意域名的请求
- Access-Control-Allow-Credentials
- 该字段可选
- 它的值是一个布尔值,表示是否允许发送Cookie
- 默认情况下,Cookie不包括在CORS请求之中
- 设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器
- 另外一方面,前端AJAX请求必须设置withCredentials = true
- 注意:如果要发送Cookie,Access-Control-Allow-Origin就不能设为星号,必须指定明确的、与请求网页一致的域名
- Access-Control-Expose-Headers
- 该字段可选
- CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma
- 前端响应如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定
简单请求响应跨域设置
前端
后端
2、非简单请求
2.1、预检请求
- 非简单请求是那种对服务器有特殊要求的请求
- 比如请求方法是PUT或DELETE
- 或者Content-Type字段的类型是
application/json
- 非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为
"预检"请求
- 浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段
- 只有得到肯定答复,浏览器才会发出正式的请求,否则就报错
举例
- HTTP请求的方法是
POST
,携带json请求体并添加自定义头信息X-Data
- 下面是这个"预检"请求的HTTP头信息
- "预检"请求用的请求方法是
OPTIONS
,表示这个请求是用来询问的。头信息里面,关键字段是Origin
,表示请求来自哪个源 - Access-Control-Request-Method
- 该字段是必须的
- 用来列出浏览器的CORS请求会用到哪些
HTTP方法
- Access-Control-Request-Headers
- 该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的
头信息字段
- 该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的
2.2、预检请求的回应
- 如果服务器否定了"预检"请求,会返回一个正常的HTTP回应,但是没有任何CORS相关的头信息字段
- 浏览器就会认定,服务器不同意预检请求
- 控制台会打印出如下的报错信息
- 如果服务器收到"预检"请求以后,检查了
Origin
、Access-Control-Request-Method
和Access-Control-Request-Headers
字段以后,确认允许跨源请求,就可以做出回应
- 关键的还是Access-Control-Allow-Origin字段
- 表示http://localhost:8081可以请求数据
- 该字段也可以设为星号,表示同意任意跨源请求
- Access-Control-Allow-Methods
- 该字段必需,它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法
- 注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次"预检"请求
- Access-Control-Max-Age
- 该字段可选,用来指定本次预检请求的
有效期
,单位为秒 - 在此期间,不用发出另一条预检请求
- 该字段可选,用来指定本次预检请求的
- Access-Control-Allow-Headers
- 该字段可选,默认情况只有几个固定请求头可以发送
- 如果前端需要发送其他请求头,就必须在Access-Control-Allow-Headers里面指定
2.3、浏览器的正常请求和回应
- 一旦服务器通过了"预检"请求,以后每次浏览器正常的CORS请求,就都跟简单请求一样,会有一个
Origin
头信息字段 - 服务器的回应,也都会有一个
Access-Control-Allow-Origin
头信息字段
"预检"请求之后,浏览器的正常CORS请求
- 头信息的Origin字段是浏览器自动添加的
服务器正常的回应
- Access-Control-Allow-Origin字段是每次回应都必定包含的
非简单请求响应跨域设置
前端
后端
3、自定义跨域过滤器
@Component
public class CrosFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletResponse response = (HttpServletResponse) servletResponse;
HttpServletRequest request =(HttpServletRequest) servletRequest;
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Methods", "*");
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader("Access-Control-Allow-Headers", "*");
/**
* 如果设置Access-Control-Allow-Credentials,允许跨域请求携带cookie
* 那么Access-Control-Allow-Origin不能为*,只能为指定的域名
*/
// response.setHeader("Access-Control-Allow-Credentials", "true");
// 非预检请求,放行即可,预检请求,则到此结束,不需要放行
if (!"OPTIONS".equalsIgnoreCase(request.getMethod())) {
filterChain.doFilter(servletRequest, servletResponse);
}
}
}
三、解决方式
1、@CrossOrigin注解
@CrossOrigin源码
- @CrossOrigin可以用在被
RequestMapping修饰的方法
和Controller类上
- 添加此注解即可实现跨域请求
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CrossOrigin {
@AliasFor("origins")
String[] value() default {};
@AliasFor("value")
String[] origins() default {};
/**
* @since 5.3
*/
String[] originPatterns() default {};
String[] allowedHeaders() default {};
String[] exposedHeaders() default {};
RequestMethod[] methods() default {};
String allowCredentials() default "";
long maxAge() default -1;
}
@CrossOrigin属性介绍
- origins和value
- 支持的源,origins和value都是相同的配置,互为别名,
默认配置是“*”
- 表示服务器支持所有源的跨域请求,安全信息较低
- 最好根据实际情况设置对应的信息(协议 + 域名 + 端口)
- 支持的源,origins和value都是相同的配置,互为别名,
- originPatterns
- 同样表示支持的源,Spring 5.3 引入的属性,默认为空
- 与origins二选一,该字段为list,也就是可以配置多个
- allowedHeaders
- 允许跨域的请求头信息,
默认为“*”
表示允许所有的请求头 - 默认前端可以发送的请求头:Cache-Control、Content-Language、Expires、Last-Modified、Pragma
- 允许跨域的请求头信息,
- exposedHeaders
- 服务器允许客户端获取的响应头,默认为空
- 默认前端可以获取的响应头:Cache-Control、Content-Language、Expires、Last-Modified、Pragm
- methods
- 服务器允许的Http Request类型,默认是允许GET、POST、HEAD
- allowCredentials
- 浏览器是否需要把凭证(如:cookies、CSRF tokens)发送到服务器,默认是关闭的
- 该选项开启后会与配置的源建立高度信任的关系,并且还会暴露一些敏感信息,所以
开启该选项时origin不允许设置为“*”
- maxAge
- “预检”结果的缓存时间,单位是秒
- 默认
1800s
,在缓存时间内同一请求不需要“预检”请求
@CrossOrigin注解比较适用于较细粒度的跨域控制
2、CorsRegistry方式
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
//是否发送Cookie
// .allowCredentials(true)
//放行哪些原始域
.allowedOrigins("*")
.allowedMethods("*")
.allowedHeaders("*")
.exposedHeaders("*");
}
}
3、CorsFilter过滤器
- 如果设置Access-Control-Allow-Credentials为true,允许跨域请求携带cookie
- 但是Access-Control-Allow-Origin不能为*,只能为指定的域名
@Configuration
public class GlobalCorsConfig {
@Bean
public CorsFilter corsFilter() {
// 1. 添加 CORS配置信息
CorsConfiguration config = new CorsConfiguration();
// 放行哪些原始域
config.addAllowedOrigin("*");
// 是否发送 Cookie
// config.setAllowCredentials(true);
// 放行哪些请求方式
config.addAllowedMethod("*");
// 放行哪些原始请求头部信息
config.addAllowedHeader("*");
// 暴露哪些头部信息
config.addExposedHeader("*");
// 2. 添加映射路径
UrlBasedCorsConfigurationSource corsConfigurationSource = new UrlBasedCorsConfigurationSource();
corsConfigurationSource.registerCorsConfiguration("/**", config);
// 3. 返回新的CorsFilter
return new CorsFilter(corsConfigurationSource);
}
}
3.1、源码解析
CorsFilter过滤器
DefaultCorsProcessor
DefaultCorsProcessor类的handleInternal方法
protected boolean handleInternal(ServerHttpRequest request, ServerHttpResponse response,
CorsConfiguration config, boolean preFlightRequest) throws IOException {
String requestOrigin = request.getHeaders().getOrigin();
// 从配置类CorsConfiguration获取允许的域名
String allowOrigin = checkOrigin(config, requestOrigin);
HttpHeaders responseHeaders = response.getHeaders();
if (allowOrigin == null) {
logger.debug("Reject: '" + requestOrigin + "' origin is not allowed");
rejectRequest(response);
return false;
}
// 获取请求的请求方法
HttpMethod requestMethod = getMethodToUse(request, preFlightRequest);
// 从配置类CorsConfiguration获取允许的请求方法
List<HttpMethod> allowMethods = checkMethods(config, requestMethod);
if (allowMethods == null) {
logger.debug("Reject: HTTP '" + requestMethod + "' is not allowed");
rejectRequest(response);
return false;
}
// 获取请求头
List<String> requestHeaders = getHeadersToUse(request, preFlightRequest);
// 从配置类CorsConfiguration获取允许的请求头
List<String> allowHeaders = checkHeaders(config, requestHeaders);
if (preFlightRequest && allowHeaders == null) {
logger.debug("Reject: headers '" + requestHeaders + "' are not allowed");
rejectRequest(response);
return false;
}
// 设置响应允许域名
responseHeaders.setAccessControlAllowOrigin(allowOrigin);
// 如果是预检请求,设置允许请求方法
if (preFlightRequest) {
responseHeaders.setAccessControlAllowMethods(allowMethods);
}
// 设置允许请求头
if (preFlightRequest && !allowHeaders.isEmpty()) {
responseHeaders.setAccessControlAllowHeaders(allowHeaders);
}
// 设置允许响应头
if (!CollectionUtils.isEmpty(config.getExposedHeaders())) {
responseHeaders.setAccessControlExposeHeaders(config.getExposedHeaders());
}
// 设置是否允许请求携带cookie
if (Boolean.TRUE.equals(config.getAllowCredentials())) {
responseHeaders.setAccessControlAllowCredentials(true);
}
// 设置预检到期时间
if (preFlightRequest && config.getMaxAge() != null) {
responseHeaders.setAccessControlMaxAge(config.getMaxAge());
}
response.flush();
return true;
}
- allowedOrigin:允许域名无论什么情况都会返回
- 只有预检请求,才有可能设置允许请求方法,请求头,响应头
- 是否允许携带cookie和到期时间,有则返回
CorsRegistry原理
- CorsRegistry方式最终也是通过DefaultCorsProcessor类实现
- 与CorsFilter不同的地方
- CorsFilter直接将属性添加到CorsConfiguration配置类
- CorsRegistry则是通过CorsRegistration类set到CorsConfiguration配置类
4、CorsWebFilter网关WebFlux过滤器
- reactor下的类,非mvc
@Configuration
public class CorsConfigure {
@Bean
public CorsWebFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedOrigin("*");
config.addAllowedMethod("*");
config.addAllowedHeader("*");
config.setAllowCredentials(true);
UrlBasedCorsConfigurationSource configSource = new UrlBasedCorsConfigurationSource(new PathPatternParser());
configSource.registerCorsConfiguration("/**", config);
return new CorsWebFilter(configSource);
}
}