跨域的那些事 - CorsWebFilter 跨域源码分析(二)

前言:

不懂基础的先看下下面的参考文章

参考: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

上一篇:跨域的那些事 - 使用场景分析(一)

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值