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

背景

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

1.跨域

理解同源策略是理解跨域的前提。同源策略定义如下: 在同一来源的页面和脚本之间进行数据交互时,浏览器会默认允许操作,而不会造成跨站脚本攻击;不同源之间进行限制。
不同源之间形成跨域,包括:协议、域名、端口。http和https,localhost和127.0.0.1也会形成跨域(即使经过域名解析后相同)。
由于浏览器引擎实现了同源策略,即对跨域访问进行了限制,因此存在跨域问题。

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

案例说明:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1jqAhIfM-1698924088095)(C:\Users\0216001379\AppData\Roaming\Typora\typora-user-images\1698912345318.png)]

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 {
    @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注解即可解决跨域问题。
如下所示:

@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加在了拦截器尾部。
执行顺序不同,业务上可能会引入问题。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/188843.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

柑橘病害数据集(四类图像分类,没有打yolo标签)

1.文件夹分为训练集和测试集 在这个数据集中&#xff0c;有一类是新鲜柑橘&#xff0c;还有另外三种疾病&#xff0c;溃疡病、黑斑病和绿化病。 2.train文件夹 2.1.blackspot&#xff08;黑斑病&#xff09; 文件夹 206张照片 2.2.canker&#xff08;溃疡病&#xff09; 文…

发布鸿蒙的第一个java应用

1.下载和安装华为自己的app开发软件DevEco Studio HUAWEI DevEco Studio和SDK下载和升级 | HarmonyOS开发者 2.打开IDE新建工程&#xff08;当前用的IDEA 3.1.1 Release&#xff09; 选择第一个&#xff0c;其他的默认只能用(API9)版本&#xff0c;搞了半天才发现8&#xff…

PPT 遇到问题总结(修改页码统计)

PPT常见问题 1. 修改页码自动计数 1. 修改页码自动计数 点击 视图——>幻灯片母版——>下翻找到计数页直接修改——>关闭母版视图

HarmonyOS4.0系列——02、汉化插件、声明式开发范式ArkTS和类web开发范式

编辑器调整 我们在每次退出编辑器后再次打开会直接进入项目文件中&#xff0c;这样在新建项目用起来很是不方便&#xff0c;所以这里跟着设置一下就好 这样下次进入就不会直接跳转到当时的文件项目中&#xff01;&#xff01; 关于汉化 settings → plugins → installe…

时间序列预测实战(十九)魔改Informer模型进行滚动长期预测(科研版本)

论文地址->Informer论文地址PDF点击即可阅读 代码地址-> 论文官方代码地址点击即可跳转下载GIthub链接 个人魔改版本地址-> 文章末尾 一、本文介绍 在之前的文章中我们已经讲过Informer模型了&#xff0c;但是呢官方的预测功能开发的很简陋只能设定固定长度去预测未…

【数据结构实验】查找(二)基于线性探测法的散列表

文章目录 1. 引言2. 实验原理2.1 散列表2.2 线性探测法 3. 实验内容3.1 实验题目&#xff08;一&#xff09;输入要求&#xff08;二&#xff09;输出要求 3.2 算法实现三、实验设计3.3 代码整合 4. 实验结果 1. 引言 本实验将通过C语言实现基于线性探测法的散列表 2. 实验原理…

一、TIDB基础

TIDB整个逻辑架构跟MYSQL类似&#xff0c;如下&#xff1a; TIDB集群&#xff1a;相当于MYSQL的数据库服务器&#xff0c;区别是MYSQL数据库服务器为单进程的&#xff0c;TIDB集群为分布式多进程的。 数据库&#xff1a;同MYSQL数据库&#xff0c;数据库属于集群&#xff0c;…

leetcode刷题日志-15.三数之和

这道题还是有点难度&#xff0c;我能想到的就是三重循环&#xff0c;但是题目限制不能重复&#xff0c;所以这道题三重循环完还要去重&#xff0c;太过于麻烦。看了题解以后&#xff0c;大佬们还是厉害&#xff0c;大概思路是这样子的&#xff1a;先对数组进行排序&#xff0c;…

【黑马甄选离线数仓day05_核销主题域开发】

1. 指标分类 ​ 通过沟通调研&#xff0c;把需求进行分析、抽象和总结&#xff0c;整理成指标列表。指标有原子指标、派生指标、 衍生指标三种类型。 ​ 原子指标基于某一业务过程的度量值&#xff0c;是业务定义中不可再拆解的指标&#xff0c;原子指标的核心功能就是对指标…

2 时间序列预测入门:GRU

0 论文地址 GRU 原论文&#xff1a;https://arxiv.org/pdf/1406.1078v3.pdf GRU&#xff08;Gate Recurrent Unit&#xff09;是循环神经网络&#xff08;RNN&#xff09;的一种&#xff0c;可以解决RNN中不能长期记忆和反向传播中的梯度等问题&#xff0c;与LSTM的作用类似&a…

已解决:Could not find a package configuration file provided by “gazebo_plugins“

问题出现在我使用catkin_make的时候 CMake Error at /home/hiuching-g/catkin_ws/devel/share/catkin/cmake/catkinConfig.cmake:83 (find_package):Could not find a package configuration file provided by "gazebo_plugins"with any of the following names:gaz…

软著项目推荐 深度学习 opencv python 实现中国交通标志识别

文章目录 0 前言1 yolov5实现中国交通标志检测2.算法原理2.1 算法简介2.2网络架构2.3 关键代码 3 数据集处理3.1 VOC格式介绍3.2 将中国交通标志检测数据集CCTSDB数据转换成VOC数据格式3.3 手动标注数据集 4 模型训练5 实现效果5.1 视频效果 6 最后 0 前言 &#x1f525; 优质…

前端vue3——html2canvas给网站截图生成宣传海报

文章目录 ⭐前言⭐选择html2canvas实现网页截图&#x1f496; 截图 ⭐图片url截图显示不出来问题&#x1f496; 解决 ⭐最终效果&#x1f496; 定义海报 ⭐总结⭐结束 ⭐前言 大家好&#xff0c;我是yma16&#xff0c;本文分享关于 前端vue3——html2canvas给网站截图生成宣传…

GIT版本控制和常用命令使用介绍

GIT版本控制和常用命令使用介绍 1. 版本控制1.1 历史背景1.2 什么是版本控制1.3 常见版本控制工具1.4 版本控制的分类 2 Git介绍2.1 Git 工作流程2.2 基本概念2.3 文件的四种状态2.4 忽略文件2.5 Git命令2.5.1 查看本地git配置命令2.5.2 远程库信息查看命令2.5.3 分支交互命令2…

【UGUI】制作用户注册UI界面

这里面主要的操作思想就是 1.打组 同一个事情里面包含两个UI元素都应该打组便于管理和查找 2.设置锚点位置 每次创建一个UI都应该设置他的锚点以便于跟随画布控制自己的&#xff1a;相对位置 3. 设置尺寸&#xff08;像素大小&#xff09; 每一次UI元素哪怕是作为父物体的…

AI和人工智能与机器学习全景报告

今天分享的是AI系列深度研究报告&#xff1a;《AI和人工智能与机器学习全景报告》。 &#xff08;报告出品方&#xff1a;appen&#xff09; 报告共计&#xff1a;30页 获取 数据获取仍是AI应用构建团队的主要瓶颈。 原因各不相同。例如&#xff0c;特定用例的数据可能不足…

【2023 云栖】阿里云田奇铣:大模型驱动 DataWorks 数据开发治理平台智能化升级

云布道师 本文根据 2023 云栖大会演讲实录整理而成&#xff0c;演讲信息如下&#xff1a; 演讲人&#xff1a;田奇铣 | 阿里云 DataWorks 产品负责人 演讲主题&#xff1a;大模型驱动 DataWorks 数据开发治理平台智能化升级 随着大模型掀起 AI 技术革新浪潮&#xff0c;大数…

nrm安装及使用

一、介绍 nrm 是一个 Node.js 的 registry 管理工具&#xff0c;它允许你快速地在不同的 npm registry 之间进行切换。通过使用 nrm&#xff0c;你可以方便地将 npm 的 registry 切换为淘宝镜像、npm 官方镜像或者其他定制的镜像&#xff0c;以加快包的下载速度。nrm仓库请点击…

leetcode-python刷题集

系列文章目录 记录一下自己的学习过程 文章目录 系列文章目录基础知识链表链表两数相加 回文 11.16罗马数字转换 11.16最长公共前缀 11.16队列 基础知识 链表 节点定义&#xff1a; class Node:def __init__(self,data None, next None):self.data dataself.next next节…

Fedora 36 ARM 镜像源更换与软件安装

1、什么是Fedora Fedora Linux是较具知名度的Linux发行套件之一&#xff0c;由Fedora专案社群开发、红帽公司赞助&#xff0c;目标是建立一套新颖、多功能并且自由的作业系统。 Fedora是商业化的Red Hat Enterprise Linux发行版的上游原始码。 2、Fedora软件安装 64 位 .deb&a…