前言:
不懂基础的先看下下面的参考文章
参考:https://juejin.cn/post/6844904055148380173、
https://juejin.cn/post/6844903678965448718、
https://www.ruanyifeng.com/blog/2016/04/cors.html
一、项目准备
spring boot
jdk1.8
IDEA
postman
提供的一个简单的健康检查接口
@RestController
@Slf4j
public class HealthCheckController {
/**
* slb 网关http 检查心跳 url
*/
private static final String HEALTH_CHECK = "/health/check";
/**
* slb 网关心跳 检查
* @return
*/
@ResponseBody
@RequestMapping(value = {HEALTH_CHECK}, method = RequestMethod.GET)
public ALResponse healthCheck(){
return ALResponse.SUCCESS;
}
}
@Configuration
public class WebFluxConfiguration implements WebFluxConfigurer {
/**
* 跨域访问
* @return
*/
@Bean
public CorsWebFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
// config.setAllowedOrigins(Collections.singletonList("*")); // 允许所有请求来源
// config.setAllowCredentials(true); // 允许发送 Cookie
// config.addAllowedMethod("*"); // 允许所有请求 Method
// config.setAllowedHeaders(Collections.singletonList("*")); // 允许所有请求 Header
// // config.setExposedHeaders(Collections.singletonList("*")); // 允许所有响应 Header
// config.setMaxAge(1800L); // 有效期 1800 秒
// 拦截所有请求,并且注释掉了所有需要配置的权限,现在等同于注入一个空配置(未配置任何访问权限许可)
source.registerCorsConfiguration("/**", config);
return new CorsWebFilter(source); // 创建 CorsFilter 过滤器
}
}
二、源码分析
org.springframework.web.cors.reactive.CorsWebFilter#filter
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
if (CorsUtils.isCorsRequest(request)) { //《1》
CorsConfiguration corsConfiguration = this.configSource.getCorsConfiguration(exchange); //《2》
if (corsConfiguration != null) {
boolean isValid = this.processor.process(corsConfiguration, exchange); //《3》
if (!isValid || CorsUtils.isPreFlightRequest(request)) {
return Mono.empty();
}
}
}
return chain.filter(exchange);
}
1、判断是否是跨域请求,封装跨域访问权限许可
public static boolean isCorsRequest(ServerHttpRequest request) {
return (request.getHeaders().get(HttpHeaders.ORIGIN) != null);
}
@Override
@Nullable
public CorsConfiguration getCorsConfiguration(ServerWebExchange exchange) {
PathContainer lookupPath = exchange.getRequest().getPath().pathWithinApplication();
return this.corsConfigurations.entrySet().stream()
.filter(entry -> entry.getKey().matches(lookupPath))
.map(Map.Entry::getValue)
.findFirst()
.orElse(null);
}
从Web过滤器开始看起
《1》首先第一步就是判断请求头部是否设置有 Origin
,如果没有设置是直接放行的,不会做任何拦截处理
《2》获取当前请求服务器的权限许可,通俗讲就是根据当前请求path,去查询服务器分配的权限。由于我们没有设置任何权限,所以任何请求都是没有权限的,获取到的空空如也
《3》核心处理器部分,下面慢慢讲
2、跨域处理
org.springframework.web.cors.reactive.DefaultCorsProcessor#process
public boolean process(@Nullable CorsConfiguration config, ServerWebExchange exchange) {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
if (!CorsUtils.isCorsRequest(request)) { //《1》
return true;
}
if (responseHasCors(response)) { //《2》
logger.trace("Skip: response already contains \"Access-Control-Allow-Origin\"");
return true;
}
if (CorsUtils.isSameOrigin(request)) { //《3》
logger.trace("Skip: request is from same origin");
return true;
}
boolean preFlightRequest = CorsUtils.isPreFlightRequest(request); //《4》
if (config == null) {
if (preFlightRequest) {
rejectRequest(response);
return false;
}
else {
return true;
}
}
return handleInternal(exchange, config, preFlightRequest); //《5》核心
}
《1》此处为什么要再次判断是否是跨域请求呢。因为防止有多个跨域处理器之间产生干扰
《2》同上,防止已经经过跨域处理器设置过访问许可了 Access-Control-Allow-Origin,如果设置了直接就不处理了
private boolean responseHasCors(ServerHttpResponse response) {
return response.getHeaders().getFirst(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN) != null;
}
《3》此处是核心判断,判断是否是同源请求,判断依据就是我们上一讲说的三要素:协议、主机、端口。只要三个不完全相等,就算是跨域访问
public static boolean isSameOrigin(ServerHttpRequest request) {
String origin = request.getHeaders().getOrigin();
if (origin == null) {
return true;
}
URI uri = request.getURI();
String actualScheme = uri.getScheme();
String actualHost = uri.getHost();
int actualPort = getPort(uri.getScheme(), uri.getPort());
Assert.notNull(actualScheme, "Actual request scheme must not be null");
Assert.notNull(actualHost, "Actual request host must not be null");
Assert.isTrue(actualPort != -1, "Actual request port must not be undefined");
UriComponents originUrl = UriComponentsBuilder.fromOriginHeader(origin).build();
// 判断 协议、主机、端口。不会转换,拿到是什么就是什么只要三个不完全相等,就算是跨域访问(即使一个用ip,一个用对应的域名,也是不相等的)
return (actualScheme.equals(originUrl.getScheme()) &&
actualHost.equals(originUrl.getHost()) &&
actualPort == getPort(originUrl.getScheme(), originUrl.getPort()));
}
《4》此处主要是判断是否是预请求 HttpMethod.OPTIONS
。复杂请求会再真正发起业务请求前,先提前预制一个探针请求,通过预制请求做探针先探测一下是否有范围权限。如果没有就不需要发起真正的业务请求了。
public static boolean isPreFlightRequest(ServerHttpRequest request) {
return (request.getMethod() == HttpMethod.OPTIONS && isCorsRequest(request) &&
request.getHeaders().get(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD) != null);
}
《5》这一步比较核心,之前的操作只能算作排雷,可以过滤掉大部分不需要处理的请求
3、DefaultCorsProcessor#handleInternal 核心处理
protected boolean handleInternal(ServerWebExchange exchange,
CorsConfiguration config, boolean preFlightRequest) {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
HttpHeaders responseHeaders = response.getHeaders();
//《1》
response.getHeaders().addAll(HttpHeaders.VARY, Arrays.asList(HttpHeaders.ORIGIN,
HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS));
//《2》
String requestOrigin = request.getHeaders().getOrigin();
String allowOrigin = checkOrigin(config, requestOrigin);
if (allowOrigin == null) {
logger.debug("Reject: '" + requestOrigin + "' origin is not allowed");
rejectRequest(response);
return false;
}
//《3》
HttpMethod requestMethod = getMethodToUse(request, preFlightRequest);
List<HttpMethod> allowMethods = checkMethods(config, requestMethod);
if (allowMethods == null) {
logger.debug("Reject: HTTP '" + requestMethod + "' is not allowed");
rejectRequest(response);
return false;
}
//《4》
List<String> requestHeaders = getHeadersToUse(request, preFlightRequest);
List<String> allowHeaders = checkHeaders(config, requestHeaders);
if (preFlightRequest && allowHeaders == null) {
logger.debug("Reject: headers '" + requestHeaders + "' are not allowed");
rejectRequest(response);
return false;
}
//《5》
responseHeaders.setAccessControlAllowOrigin(allowOrigin);
if (preFlightRequest) {
responseHeaders.setAccessControlAllowMethods(allowMethods);
}
if (preFlightRequest && !allowHeaders.isEmpty()) {
responseHeaders.setAccessControlAllowHeaders(allowHeaders);
}
if (!CollectionUtils.isEmpty(config.getExposedHeaders())) {
responseHeaders.setAccessControlExposeHeaders(config.getExposedHeaders());
}
if (Boolean.TRUE.equals(config.getAllowCredentials())) {
responseHeaders.setAccessControlAllowCredentials(true);
}
if (preFlightRequest && config.getMaxAge() != null) {
responseHeaders.setAccessControlMaxAge(config.getMaxAge());
}
return true;
}
《1》添加响应头 Vary 列表值为 Origin、Access-Control-Request-Method、Access-Control-Request-Headers
《2》检查请求的 Origin 是否在权限许可范围内,如果不在直接返回 403,一般这一步被拦截,浏览器也会作提示处理
《3》《4》 同上分别判断 Access-Control-Request-Method、Access-Control-Request-Headers 权限是否被许可
《5》经过上面的一系列判断走到底5步的,就是有权限访问的,那么呢,得告知浏览器,请求是被许可的。怎么告诉呢?
通过设置响应头部
Access-Control-Allow-Origin
Access-Control-Allow-Methods
Access-Control-Allow-Headers
Access-Control-Expose-Headers
Access-Control-Allow-Credentials
Access-Control-Max-Age
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NFiA75rt-1618484404419)(typora-user-images/image-20210415171057401.png)]
三、总结
跨域其实是一种应用场景,浏览器为了安全考虑,针对这种场景设置了一套安全机制用来保护网络访问。
浏览器做的事情,仅仅只是将每种请求进行打标(Origin 标识来源),以及对响应数据是否需要给到用户(前端程序)做一定的控制。具体权限下放到被访问到的服务端去设置,需要服务端配合浏览器定义的规范给出所配置的访问权限许可。
简单来说,浏览器负责达打标,以及数据是否要给用户控制;服务端负责对每个请求进行权限控制,是否允许浏览器将数据给到用户,通过浏览器定义的内置规范进行通信
浏览器请求时申请访问权限,需要设置的header信息
Origin
Access-Control-Request-Headers
Access-Control-Request-Method
服务端响应告知客户端,自身的权限许可范围,设置的header信息
Access-Control-Allow-Credentials
Access-Control-Allow-Headers
Access-Control-Allow-Methods
Access-Control-Allow-Origin