一、关于什么是跨域及解决方法看这篇文章

文章

二、关于@CrossOrigin

  1. 简单请求和非简单请求
  • 对于简单请求(GET,HEAD,POST),浏览器直接发出CORS请求。具体来说,就是在头信息之中,增加一个Origin字段。
  • 非简单请求是那种对服务器有特殊要求的请求,比如请求方法是PUT或DELETE,或者Content-Type字段的类型是application/json。非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为预检请求 (preflight)
  1. @CrossOrigin源码
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CrossOrigin {
	@Deprecated
	String[] DEFAULT_ORIGINS = {"*"};

	@Deprecated
	String[] DEFAULT_ALLOWED_HEADERS = {"*"};

	@Deprecated
	boolean DEFAULT_ALLOW_CREDENTIALS = false;

	@Deprecated
	long DEFAULT_MAX_AGE = 1800;

	@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注解可以标注在类或者方法上,其中几个常量如DEFAULT_ORIGINS已经在Spring 5.0弃用,取而代之的是CorsConfiguration#applyPermitDefaultValues方法。

  1. @CrossOrigin属性介绍
  • origins和value

支持的源,origins和value都是相同的配置,互为别名,默认配置是“*”,表示服务器支持所有源的跨域请求,安全信息较低,最好根据实际情况设置对应的信息(协议 + 域名 + 端口)。

  • originPatterns

同样表示支持的源,Spring 5.3 引入的属性,默认为空,与origins二选一,支持通配符的形式配置origins,比如https://*.domain1.com,该字段为list,也就是可以配置多个。

  • allowedHeaders

允许跨域的请求头信息,默认为“*”表示允许所有的请求头,CORS默认支持的请求头为:Cache-Control、Content-Language、Expires、Last-Modified、Pragma,如果你需要携带其他的请求头需要设置该属性。

  • exposedHeaders

服务器允许客户端访问的相应头,默认为空,表示只允许访问:Cache-Control、Content-Language、Expires、Last-Modified、Pragma,如果需要客户端访问其他的相应头需要设置该属性。

  • methods

服务器允许的Http Request类型,默认是允许GET、POST、HEAD,根据项目需要自行设置。

  • allowCredentials

浏览器是否需要把凭证(如:cookies、CSRF tokens)发送到服务器,默认是关闭的,因为该选项开启后会与配置的源建立高度信任的关系,并且还会暴露一些敏感信息,所以开启该选项时origin不允许设置为“*”。

  • maxAge

“预检”结果的缓存时间,单位是秒,默认1800s,在缓存时间内同一请求不需要“预检”请求。

  1. @CrossOrigin用法

标注在类上,该类的所有方法均会生效

@RestController
@RequestMapping("/account")
public class AccountController {
    @CrossOrigin
    @GetMapping("/{id}")
    public Account retrieve(@PathVariable Long id) {
        // ...
    }
    @DeleteMapping("/{id}")
    public void remove(@PathVariable Long id) {
        // ...
    }
}

同时也可以类和方法结合使用

@CrossOrigin(maxAge = 3600)
@RestController
@RequestMapping("/account")
public class AccountController {
    @CrossOrigin("https://domain2.com")
    @GetMapping("/{id}")
    public Account retrieve(@PathVariable Long id) {
        // ...
    }
    @DeleteMapping("/{id}")
    public void remove(@PathVariable Long id) {
        // ...
    }
}

@CrossOrigin注解比较适用于较细粒度的跨域控制,对于全局的跨域控制,SpringMVC提供了Global Configuration配置。

  1. Global Configuration

Spring Mvc对于全局的CORS比较简单,分为两个方案

  • 实现WebMvcConfigurer接口

@Configuration
@EnableWebMvc
//创建WebConfig类实现WebMvcConfigurer接口,通过CorsRegistry设置跨域信息
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {

        registry.addMapping("/api/**")
            .allowedOrigins("https://domain2.com")
            .allowedMethods("PUT", "DELETE")
            .allowedHeaders("header1", "header2", "header3")
            .exposedHeaders("header1", "header2")
            .allowCredentials(true).maxAge(3600);

        // Add more mappings...
    }
}
  • 通过CorsFilter
@Bean
//通过CorsConfiguration设置跨域信息,并将CorsConfiguration通过CorsFilter构造函数传递进去
CorsFilter corsFilter(){
    CorsConfiguration config = new CorsConfiguration();

    // Possibly...
    // config.applyPermitDefaultValues()

    config.setAllowCredentials(true);
    config.addAllowedOrigin("https://domain1.com");
    config.addAllowedHeader("*");
    config.addAllowedMethod("*");

    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", config);

    return new CorsFilter(source);
}

三、@CrossOrigin实现跨域原理

在这里插入图片描述

  1. SpringMVC的请求流程的源码中有这么一段,在获取执行链中
@Override
public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
    Object handler = getHandlerInternal(request);
    HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);
    //判断请求头中是否有ORIGIN字段
    if (CorsUtils.isCorsRequest(request)) {
        CorsConfiguration globalConfig = this.corsConfigSource.getCorsConfiguration(request);
        CorsConfiguration handlerConfig = getCorsConfiguration(handler, request);
        CorsConfiguration config = (globalConfig != null ? 
            globalConfig.combine(handlerConfig) : handlerConfig);
    //为跨域请求添加额外的interceptor
        executionChain = getCorsHandlerExecutionChain(request, executionChain, config);
    }
    return executionChain;
}

如果该请求是跨域请求,那么回带有ORIGIN字段,那么进行if,执行

 executionChain = getCorsHandlerExecutionChain(request, executionChain, config);

追踪进去:

//AbstractHandlerMapping.getCorsHandlerExecutionChainc
protected HandlerExecutionChain getCorsHandlerExecutionChain(HttpServletRequest request,
			HandlerExecutionChain chain, @Nullable CorsConfiguration config) {

	        //浏览器将CORS请求分成两类:简单请求和非简单请求。
	        //浏览器对这两种请求的处理是不一样的,
	        //非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight)
		if (CorsUtils.isPreFlightRequest(request)) {
			HandlerInterceptor[] interceptors = chain.getInterceptors();
			return new HandlerExecutionChain(new PreFlightHandler(config), interceptors);
		}
		else {
		    //增加专门处理跨域请求的Interceptor
			chain.addInterceptor(new CorsInterceptor(config));
			return chain;
		}
	}

AbstractHandlerMapping类是HanndlerMapping接口的实现类

  1. 跟踪再进入chain.addInterceptor(new CorsInterceptor(config));
  • CorsProcessor类

上面我们提到当执行getCorsHandlerExecutionChain方法时,若是CORS请求,则在 拦截器处理链链首 添加CORS拦截器。

chain.addInterceptor(new CorsInterceptor(config));

在这里插入图片描述

CorsInterceptor拦截器为内部类,其核心方法为preHandle,主要是调用了

this.corsProcessor.processRequest(this.config, request, response)

CorsProcessor只是接口,其具体实现类为DefaultCorsProcessor

DefaultCorsProcessor.processRequest具体实现如下

    public boolean processRequest(@Nullable CorsConfiguration config, HttpServletRequest request, HttpServletResponse response) throws IOException {
        // 对于CORS请求,需要通过CORS相关Header帮助判断是否直接返回缓存即可
        Collection<String> varyHeaders = response.getHeaders("Vary");
        if (!varyHeaders.contains("Origin")) {
            response.addHeader("Vary", "Origin");
        }
        if (!varyHeaders.contains("Access-Control-Request-Method")) {
            response.addHeader("Vary", "Access-Control-Request-Method");
        }
        if (!varyHeaders.contains("Access-Control-Request-Headers")) {
            response.addHeader("Vary", "Access-Control-Request-Headers");
        }
        
        // 这部分检测如果不是CORS请求,则直接返回true
        if (!CorsUtils.isCorsRequest(request)) {
            return true;
        } else if (response.getHeader("Access-Control-Allow-Origin") != null) {
            // 如果已经存相关HEADER,代表前面已经有Filter/Inteceptor进行了CORS相关设置
            logger.trace("Skip: response already contains \"Access-Control-Allow-Origin\"");
            return true;
        } else {
            // 判断是否为CORS预检请求——OPTIONS方法
            boolean preFlightRequest = CorsUtils.isPreFlightRequest(request);
            if (config == null) {
                if (preFlightRequest) {
                    this.rejectRequest(new ServletServerHttpResponse(response));
                    return false;
                } else {
                    return true;
                }
            } else {
                // 处理逻辑核心,请看具体实现
                return this.handleInternal(new ServletServerHttpRequest(request), new ServletServerHttpResponse(response), config, preFlightRequest);
            }
        }
    }
    
    
    // handleInternal具体实现
    protected boolean handleInternal(ServerHttpRequest request, ServerHttpResponse response, CorsConfiguration config, boolean preFlightRequest) throws IOException {
        String requestOrigin = request.getHeaders().getOrigin();
        String allowOrigin = this.checkOrigin(config, requestOrigin);       // 判断是否匹配Origin,不匹配则返回null
        HttpHeaders responseHeaders = response.getHeaders();
        if (allowOrigin == null) {                                          // 这里代表未成功匹配Origin,拒绝请求
            logger.debug("Reject: '" + requestOrigin + "' origin is not allowed");
            this.rejectRequest(response);
            return false;
        } else {
            HttpMethod requestMethod = this.getMethodToUse(request, preFlightRequest);
            List<HttpMethod> allowMethods = this.checkMethods(config, requestMethod);
            if (allowMethods == null) {
                logger.debug("Reject: HTTP '" + requestMethod + "' is not allowed");
                this.rejectRequest(response);
                return false;
            } else {
                // -----------------!!! 真正添加CORS 相关 Header 参数的位置 !!!-----------------
                List<String> requestHeaders = this.getHeadersToUse(request, preFlightRequest);
                List<String> allowHeaders = this.checkHeaders(config, requestHeaders);
                if (preFlightRequest && allowHeaders == null) {
                    logger.debug("Reject: headers '" + requestHeaders + "' are not allowed");
                    this.rejectRequest(response);
                    return false;
                } else {
                    // -----------------!!! 设置Access-Control-Allow-Origin !!!-----------------
                    responseHeaders.setAccessControlAllowOrigin(allowOrigin);
                    if (preFlightRequest) {
                    // -----------------!!! 设置Access-Control-Allow-Methods !!!-----------------
                        responseHeaders.setAccessControlAllowMethods(allowMethods);
                    }
                    // -----------------!!! 设置Access-Control-Expose-Headers !!!-----------------
                    if (preFlightRequest && !allowHeaders.isEmpty()) {
                        responseHeaders.setAccessControlAllowHeaders(allowHeaders);
                    }
                    // -----------------!!! 设置Access-Control-Expose-Headers !!!-----------------
                    if (!CollectionUtils.isEmpty(config.getExposedHeaders())) {
                        responseHeaders.setAccessControlExposeHeaders(config.getExposedHeaders());
                    }
                    // -----------------!!! 设置Access-Control-Allow-Credentials !!!-----------------
                    if (Boolean.TRUE.equals(config.getAllowCredentials())) {
                        responseHeaders.setAccessControlAllowCredentials(true);
                    }
                    // -----------------!!! 设置Access-Control-Max-Age !!!-----------------
                    if (preFlightRequest && config.getMaxAge() != null) {
                        responseHeaders.setAccessControlMaxAge(config.getMaxAge());
                    }

                    response.flush();
                    return true;
                }
            }
        }
    }

  1. HTTP Header 中 Vary字段作用
  • Vary HTTP 响应头决定如何满足未来的请求头,以决定一个缓存的响应是否可以使用,而不是请求从源服务器一个新的一个。
  • Vary的意义在于告诉代理服务器/缓存/CDN,如何判断是否直接返回缓存资源即可。比如Vary中有User-Agent,那么即使相同的请求,如果用户使用IE打开了一个页面,再用Firefox打开这个页面的时候,CDN/代理会认为是不同的页面,如果Vary中没有User-Agent,那么CDN/代理会认为是相同的页面,直接给用户返回缓存的页面,而不会再去web服务器请求相应的页面。
  1. 思考
  • Spring Web 5.3.9版本CORS是通过CorsInterceptor实现的,CorsInterceptor实现了HandlerInterceptor接口,因此可知其实际是通过拦截器实现的。那么在Spring Web中,Filter和InterceptorInterceptor的区别是什么?
  • 过滤器: 依赖于servlet容器。在实现上基于函数回调,可以对几乎所有请求进行过滤,但是缺点是一个过滤器实例只能在容器初始化时调用一次。使用过滤器的目的是用来做一些过滤操作,获取我们想要获取的数据.比如:在过滤器中修改字符编码;在过滤器中修改 HttpServletRequest的一些参数,包括:过滤低俗文字、危险字符等

  • 拦截器: 依赖于web框架,在SpringMVC中就是依赖于SpringMVC框架。在实现上基于Java的反射机制,属于面向切面编程(AOP)的一种运用。由于拦截器是基于web框架的调用.因此可以使用spring的依赖注入(DI)进行一些业务操作,同时一个拦截器实例在一个controller生命周期之内可以多次调用。但是缺点是只能对controller请求进行拦截,对其他的一些比如直接访问静态资源的请求则没办法进行拦截处理。

  • Filter和Interceptor执行顺序?

Interceptor链先执行还是Filter链先执行?
首先,Filter是基于Servlet的,通过即< url-pattern >/hello< /url-pattern >标签产生关联,其他相关标签是< serlvet-mapping >和< filter-mapping标签 >。

<servlet-mapping>  
    <servlet-name>servlet</servlet-name>  
    <url-pattern>/hello</url-pattern>  
    <url-pattern>/world</url-pattern>
    <url-pattern>/home1</url-pattern> 
</servlet-mapping> 

<filter-mapping>
    <filter-name>filter</filter-name>
    <url-pattern>/hello</url-pattern>
    <url-pattern>/home2</url-pattern>  
</filter-mapping>

而Interceptor是基于MVC的,相关xml配置。

<!-- 拦截器 -->  
  <mvc:interceptors>  
      <!-- 对所有请求都拦截,公共拦截器可以有多个 -->  
      <bean name="baseInterceptor" class="com.scorpios.interceptor.BaseInterceptor" />  
    
    <mvc:interceptor> 
        <!-- 对/test.html进行拦截 -->       
          <mvc:mapping path="/test.html"/>  
          <!-- 特定请求的拦截器只能有一个 -->  
          <bean class="com.scorpios.interceptor.TestInterceptor" />  
      </mvc:interceptor>  
  </mvc:interceptors>

Filter的执行是有先后顺序的,根据在web.xml中配置的先后顺序。其主要有三个方法init、doFilter、destory,我们主要关注doFilter方法,在这个方法中chain.doFilter会将请求及响应发送给下一个Filter,我们在这句调用前执行前置操作及后置操作即可。

public class LoggerFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("初始化参数,在创建Filter时自动调用,当我们需要设置初始化参数的时候,可以写到该方法中");
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        String contextPath = request.getServletContext().getContextPath();
        System.out.println("执行Filter前置操作");
        chain.doFilter(request, response);
        System.out.println("执行Filter后置操作");
    }

    @Override
    public void destroy() {
        System.out.println("在销毁Filter时自动调用");
    }
 }

在这里插入图片描述

这里可以参考这篇

参考文章
参考文章

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐