SpringMVC系列-7 @CrossOrigin注解与跨域问题

背景

前段时间帮同事分析了一个跨域问题,正好系统分析和整理一下。

1.跨域

理解同源策略是理解跨域的前提。同源策略定义如下: 在同一来源的页面和脚本之间进行数据交互时,浏览器会默认允许操作,而不会造成跨站脚本攻击;不同源之间进行限制。

不同源之间形成跨域,包括:协议、域名、端口。http和https,localhost和127.0.0.1也会形成跨域(即使经过域名解析后相同)。

由于浏览器引擎实现了同源策略,即对跨域访问进行了限制,因此存在跨域问题。

注意:注意区分浏览器引擎和V8引擎的区别,浏览器引擎包括解析HTML/JS/CSS和渲染等功能,而V8只是一个JS解析器;因此:浏览器中存在跨域问题,而基于Chrome-V8的Nodejs中不存在跨域问题。

案例说明:

2.浏览器处理跨域步骤

根据请求类型不同,浏览器有不同的处理策略,可以分为简单请求和复杂请求:

2.1 简单请求

满足以下条件的为简单请求:

(1) 方法取值范围:GET,POST,HEAD;

(2) Context-Type取值范围: text/plain, application/x-www-form-urlencoded, multipart/form-data

(3) 不包含自定义头域; 即只能包含HTTP自带的Accept, Accept-Language, Content-Type ...

详见: CORS

对于简单请求,浏览器会直接向服务器发送请求。

2.2 复杂请求

简单请求之外的HTTP请求为复杂请求。此时,浏览器会正式请求之前会先发送一个OPTIONS类型的预检请求。

2.3 跨域请求头域

如果ajax是跨域请求,浏览器收到HTTP请求响应后对响应头进行分析------是否支持跨域:支持-请求正常,否则-抛出异常。

响应头包含以下几个部分:
(1) Access-Control-Allow-Origin

指定哪些域可以访问请求的资源, 多个用逗号分开; 取值为"file://"时,表示只允许来自本地文件系统的跨域请求, 而* 表示允许所有源访问。

(2) Access-Control-Allow-Credentials

取值范围有true和false; 表示是否允许客户端使用认证信息(如cookies、HTTP身份验证等)进行跨域请求。即取值为true时,客户端可以携带认证信息,如cookies,以进行身份验证和个性化等操作。

(3) Access-Control-Allow-Methods

取值范围为HTTP的方法类型,如GET和POST;指定允许的HTTP请求方法,多个使用逗号分隔。

(4) Access-Control-Allow-Headers

这个头域用于指定允许客户端访问的响应头, 多个值用逗号分隔;

例如,Access-Control-Expose-Headers: X-Custom-Header, Content-Type表示允许客户端访问X-Custom-Header和Content-Type响应头。

上述4个属性是浏览器判断是否跨域的依据。

注意:当指定多个Access-Control-Allow-Origin时,浏览器会报错如下:

shell 复制代码
Access to XMLHttpRequest at 'http://localhost:8181/a/b/c' from origin 'http://localhost:8182' has been blocked by CORS policy: 
The 'Access-Control-Allow-Origin' header contains multiple values '*, *', 
but only one is allowed.

2.4 跨域解决方式

2.4.1 前端解决跨域

前端可通过使用JSONP和代理服务器方式解决。如 vue项目中使用Axios实现跨域的原理是代理服务器,在vue项目中,通常会使用webpack-dev-server作为开发服务器,它内置了HTTP代理功能。当Axios发出跨域请求时,它会将请求发送到webpack-dev-server的代理服务器上,代理服务器将请求转发到目标服务器。在转发过程中,代理服务器会处理跨域请求,从而绕过浏览器的同源策略限制。

2.4.2 后端-服务器解决跨域

服务端处理跨域问题的核心是在HTTP响应中加入指定的响应头,使得浏览器正常校验跨域。

可通过过滤器(Filter)和SpringMVC的拦截器()来实现。

案例1-使用过滤器Filter:

Filter可以自定义,也可使用开源解决方案:

xml 复制代码
<dependency>
    <groupId>com.thetransactioncompany</groupId>
    <artifactId>cors-filter</artifactId>
    <version>2.9</version>
</dependency>

配置并注册到web容器中:

xml 复制代码
<filter>
    <filter-name>CORS</filter-name>
    <filter-class>com.thetransactioncompany.cors.CORSFilter</filter-class>
    <init-param>
        <param-name>cors.allowOrigin</param-name>
        <param-value>*</param-value>
    </init-param>
    <init-param>
        <param-name>cors.supportedMethods</param-name>
        <param-value>GET, POST, HEAD, PUT, DELETE</param-value>
    </init-param>
    <init-param>
        <param-name>cors.supportedHeaders</param-name>
        <param-value>Accept, Origin, X-Requested-With, Content-Type, Last-Modified</param-value>
    </init-param>
    <init-param>
        <param-name>cors.exposedHeaders</param-name>
        <param-value>Set-Cookie</param-value>
    </init-param>
    <init-param>
        <param-name>cors.supportsCredentials</param-name>
        <param-value>true</param-value>
    </init-param>
</filter>

<filter-mapping>
    <filter-name>CORS</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

案例2-使用拦截器:

java 复制代码
// 定义跨域拦截器
public class CrossInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        response.addHeader("Access-Control-Allow-Origin", "*");
        response.addHeader("Access-Control-Allow-Methods", "*");
        response.addHeader("Access-Control-Max-Age", "100");
        response.addHeader("Access-Control-Allow-Headers", "Content-Type");
        response.addHeader("Access-Control-Allow-Credentials", "false");
        return true;
    }
}

// 注册拦截器
@Configuration
public class MyInterceptorConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new CrossInterceptor());
    }
}

相对于过滤器和拦截器,SpringMVC提供了颗粒度更小的解决方案,使用@CrossOrigin注解即可解决跨域问题。

如下所示:

java 复制代码
@RestController
@RequestMapping("/api/crossDemo")
public class CrossController {
    @CrossOrigin(origins = "*", maxAge = 3600)
    @RequestMapping(value = "/put", method = RequestMethod.PUT)
    public String put() {
        return "success";
    }
}

@CrossOrigin可注解在方法上对方法生效,也可作用在类上对类中所有方法生效;当类和方法都存在@CrossOrigin注解时,方法上的注解会覆盖类上的注解。

3.@CrossOrigin原理介绍

@CrossOrigin注解本质上是对SpringMVC的调用链添加一个拦截器,在拦截器中对HTTP的响应头进行跨域设置。

3.1 @CrossOrigin注解

@CrossOrigin注解包含以下属性:
[1] value和origins属性:

String[]类型; 指定允许请求源列表; 一般设置为*,表示对所有的网址开放。与Access-Control-Allow-Origin头域保持一致。

[2] allowedHeaders属性:

String[]类型;请求中允许的请求头列表。如果设置成"*",则表示允许所有的请求头。

[3] exposedHeaders属性:

String[]类型;@CrossOrigin注解的exposedHeaders属性用于指定允许暴露的响应头列表。这个属性主要用于控制客户端(如浏览器)可以访问哪些响应头。如果设置成"*",则表示允许暴露所有的响应头。

例如,假设我们有一个API接口,需要暴露响应头"Content-Length"给客户端,可以这样设置:

java 复制代码
@CrossOrigin(origins = "*", allowedHeaders = "*", exposedHeaders = "Content-Length")

在这个例子中,我们允许来自"http://example.com"的请求访问我们的API,并允许请求头"Content-Type"。同时,我们指定了响应头"Content-Length"可以被客户端访问。在实际的CORS请求中,响应头"Content-Length"将被存储在Access-Control-Expose-Headers列表中,客户端可以通过这个头获取"Content-Length"信息。

默认情况下,暴露的响应头有:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果需要暴露其他的响应头,需要在@CrossOrigin注解中显式指定。

[4] methods属性:

RequestMethod[]类型;methods属性用于指定允许的HTTP方法。它是一个字符串数组,表示允许跨域请求的HTTP方法列表。这个属性主要用于控制哪些HTTP请求方法可以被客户端(如浏览器)使用。与Access-Control-Allow-Methods头域保持一致。如果API接口只允许GET和POST方法进行跨域请求,可以按如下方式进行设置:

java 复制代码
@CrossOrigin(origins = "*", allowedHeaders = "*", methods = "GET,POST")

[5] allowCredentials属性:

String类型;与Access-Control-Allow-Credentials头域保持一致,表示是否允许携带认证信息(如cookies、HTTP身份验证等)进行跨域请求。

[6] maxAge属性:

long类型; 预检请求的有效期(单位: 秒),有效期内不必再次发送预检请求,默认是-1。

当maxAge值为-1时,表示预检请求没有有效期限制。即浏览器接收到预检响应后,无论经过多长时间,只要浏览器与服务器之间的连接保持打开状态,都不需要再次发送预检请求。

3.2 项目初始化

RequestMappingHandlerMapping 类实现了InitializingBean 接口,在初始化阶段会调用afterPropertiesSet钩子方法:

java 复制代码
public void afterPropertiesSet() {
    initHandlerMethods();
}

initHandlerMethods()方法核心是调用register方法进行Controller接口url的注册,该过程会同时设置跨域信息:

java 复制代码
public void register(T mapping, Object handler, Method method) {
    // register url 和 method关系
    CorsConfiguration corsConfig = initCorsConfiguration(handler, method, mapping);
    if (corsConfig != null) {
        this.corsLookup.put(handlerMethod, corsConfig);
    }
    // ...
}

通过initCorsConfiguration方法获取跨域配置,保存在内存(corsLookup属性)中。
initCorsConfiguration方法逻辑如下:

java 复制代码
@Override
protected CorsConfiguration initCorsConfiguration(Object handler, Method method, RequestMappingInfo mappingInfo) {
    HandlerMethod handlerMethod = createHandlerMethod(handler, method);
    Class<?> beanType = handlerMethod.getBeanType();
    // 从Controller类上获取@CrossOrigin注解
    CrossOrigin typeAnnotation = AnnotatedElementUtils.findMergedAnnotation(beanType, CrossOrigin.class);
     // 从接口方法上获取@CrossOrigin注解
    CrossOrigin methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, CrossOrigin.class);

    // 没有注解,表示不进行跨域处理
    if (typeAnnotation == null && methodAnnotation == null) {
        return null;
    }

    CorsConfiguration config = new CorsConfiguration();
    // 先根据类的注解信息进行构造,再使用方法注解信息覆盖,因此优先级方法高于类
    updateCorsConfig(config, typeAnnotation);
    updateCorsConfig(config, methodAnnotation);

    if (CollectionUtils.isEmpty(config.getAllowedMethods())) {
        for (RequestMethod allowedMethod : mappingInfo.getMethodsCondition().getMethods()) {
            config.addAllowedMethod(allowedMethod.name());
        }
    }
    // 默认设置
    return config.applyPermitDefaultValues();
}

Note: 通过注解未设置时,applyPermitDefaultValues方法进行默认设置:

allowedOrigins跨域源设置为*, allowedMethods和resolvedMethods设置为GET、HEAD、POST;allowedHeaders设置为*;maxAge设为为1800L, 即30分钟。

3.3 HTTP接口被调用

当请求进入DispatcherServlet的doDispatch方法中:

java 复制代码
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
	 //...
	 // Determine handler adapter for the current request.
	 HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
	
	 //...
	 // 调用拦截器的preHandle方法
	 if (!mappedHandler.applyPreHandle(processedRequest, response)) {
	  return;
	 }
	 
	 //...
	 // 调用目标Controller接口
	 mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
	
	 //...
}

getHandlerAdapter(mappedHandler.getHandler())根据被调用的接口获取HandlerAdapter 对象,该对象包含一个调用链,@CrossOrigin注解关联的拦截器添加在该链路中。

构造调用链的逻辑如下所示:

java 复制代码
public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
	Object handler = getHandlerInternal(request);
	// 根据被调用的Controller接口构造执行链 
	HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);
	
	// 向执行链中添加跨域拦截器
	if (hasCorsConfigurationSource(handler) || CorsUtils.isPreFlightRequest(request)) {
	CorsConfiguration config = (this.corsConfigurationSource != null ? this.corsConfigurationSource.getCorsConfiguration(request) : null);
	// 从内存中获取跨域-拦截器对象(上一节中的保存为了这里的获取)
	CorsConfiguration handlerConfig = getCorsConfiguration(handler, request);
	config = (config != null ? config.combine(handlerConfig) : handlerConfig);
	executionChain = getCorsHandlerExecutionChain(request, executionChain, config);
	}
	return executionChain;
}

至此,@CrossOrigin注解的实现原理已梳理完成。

注意 :Spring在不同版本实现有区别(最近定位问题时发现一个因版本升级导致的问题-促使我发现这个问题):

5.2.8.RELEASE中:

java 复制代码
protected HandlerExecutionChain getCorsHandlerExecutionChain(HttpServletRequest request, HandlerExecutionChain chain, @Nullable CorsConfiguration config) {
	if (CorsUtils.isPreFlightRequest(request)) {
		HandlerInterceptor[] interceptors = chain.getInterceptors();
		chain = new HandlerExecutionChain(new PreFlightHandler(config), interceptors);
	} else {
		chain.addInterceptor(0, new CorsInterceptor(config));
	}
	return chain;
}

4.3.20.RELEASE版本中:

java 复制代码
protected HandlerExecutionChain getCorsHandlerExecutionChain(HttpServletRequest request, HandlerExecutionChain chain, CorsConfiguration config) {
	if (CorsUtils.isPreFlightRequest(request)) {
		HandlerInterceptor[] interceptors = chain.getInterceptors();
		chain = new HandlerExecutionChain(new PreFlightHandler(config), interceptors);
	} else {
		chain.addInterceptor(new CorsInterceptor(config));
	}
	return chain;
}

区别在于5.2.8.RELEASE版本将跨域拦截器CorsInterceptor 放在了拦截器首部,而4.3.20.RELEASE将CorsInterceptor 加在了拦截器尾部。

执行顺序不同,业务上可能会引入问题。

相关推荐
Miketutu2 小时前
Spring MVC消息转换器
java·spring
小小虫码3 小时前
项目中用的网关Gateway及SpringCloud
spring·spring cloud·gateway
带刺的坐椅8 小时前
无耳科技 Solon v3.0.7 发布(2025农历新年版)
java·spring·mvc·solon·aop
精通HelloWorld!11 小时前
使用HttpClient和HttpRequest发送HTTP请求
java·spring boot·网络协议·spring·http
LUCIAZZZ12 小时前
基于Docker以KRaft模式快速部署Kafka
java·运维·spring·docker·容器·kafka
拾忆,想起12 小时前
如何选择Spring AOP的动态代理?JDK与CGLIB的适用场景
spring boot·后端·spring·spring cloud·微服务
鱼骨不是鱼翅14 小时前
Spring Web MVC基础第一篇
前端·spring·mvc
hong_zc16 小时前
Spring MVC (三) —— 实战演练
java·spring·mvc
Future_yzx17 小时前
Spring AOP 入门教程:基础概念与实现
java·开发语言·spring
安清h17 小时前
【基于SprintBoot+Mybatis+Mysql】电脑商城项目之用户注册
数据库·后端·mysql·spring·mybatis