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时,浏览器会报错如下:
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可以自定义,也可使用开源解决方案:
<dependency><groupId>com.thetransactioncompany</groupId><artifactId>cors-filter</artifactId><version>2.9</version>
</dependency>
配置并注册到web容器中:
<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-使用拦截器:
// 定义跨域拦截器
public class CrossInterceptor implements HandlerInterceptor {@Overridepublic 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 {@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new CrossInterceptor());}
}
相对于过滤器和拦截器,SpringMVC提供了颗粒度更小的解决方案,使用@CrossOrigin
注解即可解决跨域问题。
如下所示:
@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"给客户端,可以这样设置:
@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方法进行跨域请求,可以按如下方式进行设置:
@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
钩子方法:
public void afterPropertiesSet() {initHandlerMethods();
}
initHandlerMethods()
方法核心是调用register
方法进行Controller接口url的注册,该过程会同时设置跨域信息:
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
方法逻辑如下:
@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
方法中:
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注解关联的拦截器添加在该链路中。
构造调用链的逻辑如下所示:
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中:
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版本中:
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加在了拦截器尾部。
执行顺序不同,业务上可能会引入问题。
相关文章:

SpringMVC系列-7 @CrossOrigin注解与跨域问题
背景 前段时间帮同事分析了一个跨域问题,正好系统分析和整理一下。 1.跨域 理解同源策略是理解跨域的前提。同源策略定义如下: 在同一来源的页面和脚本之间进行数据交互时,浏览器会默认允许操作,而不会造成跨站脚本攻击&#x…...
[RK-Linux] misc分区详解
misc 其实是英文 miscellaneous 的前四个字母,杂项、混合体、大杂烩的意思。 misc 分区的概念来源于 Android 系统,Linux 系统中常用来作为系统升级时或者恢复出厂设置时使用。 misc 分区的读写:misc 分区在以下情况下会被读写。 Uboot:设备加电启动时,首先启动 Uboot,…...

用户与组管理:如何在服务器系统中管理用户和权限
你是否想过,当你登录到一个服务器系统时,你是如何被识别和授权的?你是否知道,你可以通过创建和管理用户和组来简化和优化你的系统管理工作?你是否想了解一些常用的用户和组管理命令和技巧?如果你的答案是肯…...

【负载均衡】这些内容你需要知道下
😄作者简介: 小曾同学.com,一个致力于测试开发的博主⛽️,主要职责:测试开发、CI/CD 如果文章知识点有错误的地方,还请大家指正,让我们一起学习,一起进步。 😊 座右铭:不…...

深入理解 Django 中的事务管理
概要 在数据库操作中,事务是确保数据完整性和一致性的关键机制。Django 作为一个强大的 Python Web 框架,提供了灵活而强大的事务管理功能。理解和正确使用 Django 中的事务对于开发高质量的 Web 应用至关重要。本文将深入探讨 Django 的事务管理机制&a…...
springsecurity6配置一
springsecurity6默认的过滤器是UsernamePasswordAuthenticationToken。具体操作步骤如下: 一、定义一个实体实现springsecurity的UserDetails接口 package com.school.information.core.security.entity;import com.alibaba.fastjson.annotation.JSONField; import com.scho…...
四边形不等式优化DP
目录 四边形不等式内容[HNOI2008]玩具装箱解析代码实现 参考资料 四边形不等式内容 TODO [HNOI2008]玩具装箱 解析 满足四边形不等式,决策具有单调性. 对于两个位置 i , j i, j i,j, 对应的最优决策点一定有 o p t [ i ] < o p t [ j ] opt[i] < opt[j]…...
Gin 学习笔记01-数据返回
Gin 数据返回 1、返回 string 和 json2、返回 xml 和 ymal3、返回html4、重定向 1、返回 string 和 json c.String()c.JSON() package mainimport ("github.com/gin-gonic/gin""net/http" )func getJSON(c *gin.Context) {//c.String(http.StatusOK, &qu…...

【AI考证笔记】NO.1人工智能的基础概念
目录 一、什么是智能 1.什么是智能 2.智能的种类 二、什么是人工智能 1.人工智能之父——图灵 2.人工智能的定义 三、人工智能的发展阶段 四、哪些工作要被淘汰掉? 以下部分内容来自于百度智能云人才认证培训讲义,腾讯等也有人工智能类似的讲义&…...
【Exception】npm ERR! code UNABLE_TO_GET_ISSUER_CERT_LOCALLY
Talk is cheap, show me the code. 环境 | Environment kversionOSwindows 11nodev18.14.2npm9.5.0 报错日志 | Error log >npm create vitelatest Need to install the following packages:create-vite5.0.0 Ok to proceed? (y) y npm ERR! code UNABLE_TO_GET_ISSUER_…...

持续集成交付CICD:GitLabCI 通过trigger触发流水线
目录 一、理论 1.GitLabCI 二、实验 1.搭建共享库项目 2.GitLabCI 通过trigger触发流水线 三、问题 1.项目app02未触发项目app01 2.GitLab 报502网关错误 一、理论 1.GitLabCI (1) 概念 GitLab CI(Continuous Integration)是一种持续集成工具…...
Java 多线程中的sleep()和wait()方法的区别
Java 多线程中的sleep()和wait()方法的区别 1、相同点 sleep()和wait()都可以暂停线程的执行。 2、不同点 所在类不同 sleep()是Thread类的静态方法。 wait()是Object类的方法。 锁释放不同 sleep()是不释放锁的。 wait()是释放锁的。 用途不同 sleep()常用于一定时间内暂停…...
车载以太网-数据链路层-VLAN
文章目录 车载以太网VLAN(Vehicle Ethernet VLAN)车载以太网VLAN帧格式VLAN帧报文VLAN帧报文示例车载以太网VLAN(Vehicle Ethernet VLAN) 车载以太网VLAN(Vehicle Ethernet VLAN)是一种在车辆网络中使用的虚拟局域网技术。它允许在车载以太网网络中创建多个逻辑网络,从…...

【Vue】filter的用法
上一篇: vue的指令 https://blog.csdn.net/m0_67930426/article/details/134599378?spm1001.2014.3001.5502 本篇所使用指令 v-for v-on v-html <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"&…...
python web项目导包规范
python web项目导包规范 python 内置的模块通过第三方库安装的模块框架自身提供的模块用户自己定义的模块 如: from __future__ import absolute_import, unicode_literalsfrom debug_toolbar.panels import Panelfrom django.utils.translation import ugettext_…...
AtCoder Beginner Contest 330 题解
目录 A - Counting PassesB - Minimize Abs 1C - Minimize Abs 2D - Counting LsE - Mex and Update A - Counting Passes 原题链接 题目描述 给定N个数和一个整数L,输出大于等于L的数的个数。 public static void solve() throws IOException{int n readInt(), m…...
论文速读《DeepFusion: Lidar-Camera Deep Fusion for Multi-Modal 3D Object Detection》
概括主要内容 文章《DeepFusion: Lidar-Camera Deep Fusion for Multi-Modal 3D Object Detection》提出了两种创新技术,以改善多模态3D检测模型的性能,通过更有效地融合相机和激光雷达传感器数据来提高对象检测的准确性,尤其是在行人检测方面…...
关于前端处理后端轮询的操作 (总结)
使用场景:前端首次发起请求获取数据,若失败则每隔1s发起一次知道成功获取数据为止解决方案: 使用轮询操作,涉及定时器的使用和关闭 (使用vue2代码为例) data() {return {pollingResult_en: null, // 处理轮询结果bizI…...

【SpringCloud】设计原则之单一职责与服务拆分
一、设计原则之单一职责 设计原则很重要的一点就是简单,单一职责也就是所谓的专人干专事 一个单元(一个类、函数或微服务)应该有且只有一个职责 无论如何,一个微服务不应该包含多于一个的职责 职责单一的后果之一就是职责单…...
UDP分片和丢包与TCP效果对比
UDP 分片 与 丢包,UDP 真的比 TCP 高效吗? UDP(用户数据报协议)和TCP(传输控制协议)在很多方面都有显著的区别。总体来说,TCP更适合需要可靠传输的应用,例如网页浏览、电子邮件等&a…...
浅谈 React Hooks
React Hooks 是 React 16.8 引入的一组 API,用于在函数组件中使用 state 和其他 React 特性(例如生命周期方法、context 等)。Hooks 通过简洁的函数接口,解决了状态与 UI 的高度解耦,通过函数式编程范式实现更灵活 Rea…...
Vue记事本应用实现教程
文章目录 1. 项目介绍2. 开发环境准备3. 设计应用界面4. 创建Vue实例和数据模型5. 实现记事本功能5.1 添加新记事项5.2 删除记事项5.3 清空所有记事 6. 添加样式7. 功能扩展:显示创建时间8. 功能扩展:记事项搜索9. 完整代码10. Vue知识点解析10.1 数据绑…...

springboot 百货中心供应链管理系统小程序
一、前言 随着我国经济迅速发展,人们对手机的需求越来越大,各种手机软件也都在被广泛应用,但是对于手机进行数据信息管理,对于手机的各种软件也是备受用户的喜爱,百货中心供应链管理系统被用户普遍使用,为方…...
MySQL 隔离级别:脏读、幻读及不可重复读的原理与示例
一、MySQL 隔离级别 MySQL 提供了四种隔离级别,用于控制事务之间的并发访问以及数据的可见性,不同隔离级别对脏读、幻读、不可重复读这几种并发数据问题有着不同的处理方式,具体如下: 隔离级别脏读不可重复读幻读性能特点及锁机制读未提交(READ UNCOMMITTED)允许出现允许…...
大语言模型如何处理长文本?常用文本分割技术详解
为什么需要文本分割? 引言:为什么需要文本分割?一、基础文本分割方法1. 按段落分割(Paragraph Splitting)2. 按句子分割(Sentence Splitting)二、高级文本分割策略3. 重叠分割(Sliding Window)4. 递归分割(Recursive Splitting)三、生产级工具推荐5. 使用LangChain的…...

Nuxt.js 中的路由配置详解
Nuxt.js 通过其内置的路由系统简化了应用的路由配置,使得开发者可以轻松地管理页面导航和 URL 结构。路由配置主要涉及页面组件的组织、动态路由的设置以及路由元信息的配置。 自动路由生成 Nuxt.js 会根据 pages 目录下的文件结构自动生成路由配置。每个文件都会对…...
鸿蒙中用HarmonyOS SDK应用服务 HarmonyOS5开发一个医院查看报告小程序
一、开发环境准备 工具安装: 下载安装DevEco Studio 4.0(支持HarmonyOS 5)配置HarmonyOS SDK 5.0确保Node.js版本≥14 项目初始化: ohpm init harmony/hospital-report-app 二、核心功能模块实现 1. 报告列表…...

MySQL 8.0 OCP 英文题库解析(十三)
Oracle 为庆祝 MySQL 30 周年,截止到 2025.07.31 之前。所有人均可以免费考取原价245美元的MySQL OCP 认证。 从今天开始,将英文题库免费公布出来,并进行解析,帮助大家在一个月之内轻松通过OCP认证。 本期公布试题111~120 试题1…...

IT供电系统绝缘监测及故障定位解决方案
随着新能源的快速发展,光伏电站、储能系统及充电设备已广泛应用于现代能源网络。在光伏领域,IT供电系统凭借其持续供电性好、安全性高等优势成为光伏首选,但在长期运行中,例如老化、潮湿、隐裂、机械损伤等问题会影响光伏板绝缘层…...

ArcGIS Pro制作水平横向图例+多级标注
今天介绍下载ArcGIS Pro中如何设置水平横向图例。 之前我们介绍了ArcGIS的横向图例制作:ArcGIS横向、多列图例、顺序重排、符号居中、批量更改图例符号等等(ArcGIS出图图例8大技巧),那这次我们看看ArcGIS Pro如何更加快捷的操作。…...