前言
由于前后端分离项目是两个项目,一个前端一个后端,各司其职。不同于服务端渲染项目。往往这两个项目部署在不同的服务器上,即使在同一台服务器下,端口号也是不同的,这就产生了跨域问题。
本篇文章主要介绍如何解决跨域以及跨域的原理分析,希望观众老爷们多多支持,请在评论区批评指正!
1. 什么是跨域?
浏览器处于安全的考虑,使用 XMLHttpRequest
对象发起的 HTTP
请求时必须遵循同源策略(也就是必须是相同的协议,相同的域名,相同的端口号才不为跨域),否则就是跨域的 HTTP
请求。默认情况下这种请求是被禁止的。同源策略要求源相同才能正常进行通信。
2. 解决跨域的几种方式
2.1. 前端的解决方式

前端解决跨域的方式是设置代理,设置代理只需要请求前端项目的地址(如你前端项目地址为 localhost:8080
那么你请求该地址,就会自动转发到代理服务器地址,也就是真正的服务器地址)下面主要介绍 vue
项目的代理设置。
「如果你使用的是」 vue CLI
「可以这样做:」
在 vue.config.js
中添加如下配置:
如果你只需要配置一个代理服务,可以这样做:
devServer: {
proxy: "http://localhost:5000"
}
假设你的服务分为用户和管理员服务,分布在不同的服务器上,那么你需要配置多个代理服务:
devServer: {
proxy: {
'api1': { //匹配所有以 'api1' 开头的请求路径
target: 'http://localhost:5000', //代理目标的基础路径,即真正的服务器地址
changeOrigin: true,
pathRewrite: {'^api1':''} //将请求发给服务器时,自动去掉前缀 api1
},
'api2: {
target: 'http://localhost:5001',
changeOrigin: true,
pathRewrite: {'^api2':''}
}
}
这种方法的优点可以配置多个代理,通过不同的前缀,向不同的远程服务器发送请求。
「如果你使用的是」 vite
「构建的项目可以这样做:」
在 vite.config.js
中通过 server.porxy
进行配置:
配置一个代理服务器
// vite.config.ts 代理配置
proxy: { // 代理配置
'/dev': 'https://www.baidu.com/'
},
实际的调用地址为:https://www.baidu.com/dev
假设我们的项目地址为:localhost:8080
那么向该服务器发起请求的地址为localhost:8080/dev
配置多个代理服务器
// vite.config.ts 代理配置
proxy: { // 代理配置
'/user': {
target: 'https://www.baidu.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^/user/, '')
},
'/cus': {
target: 'https://www.taobao.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^/cus/, '')
}
}
实际调用为 https://www.baidu.com/user
,https://www.taobao.com/cus
假设我们的项目地址为:localhost:8080
那么向这两个服务器发起请求的地址为localhost:8080/user``localhost:8080/cus
「注意」
当前端项目上线后,需要在 nginx
中配置反向代理。上述的代理只用于开发环境。
1.2. 后端的解决方式
通过CORS
来解决,CORS
是一个 W3C
标准,全称跨域资源共享(Cross-origin resource sharing
),允许浏览器向跨源服务器,发送 XMLHttpRequest
请求,从而解决同源限制。
它通过服务器增加一个特殊的请求头: Header[Access-Control-Allow-Origin]
来告诉客户端跨域的限制,如果浏览器支持 CORS
,并且 Origin
通过的话,就会允许 XMLHttpRequest
请求跨域。
「SpringBoot 中使用 CORS 解决跨域」
-
使用 @CrossOrigin
注解:这个类可以标志在类上或者方法上

我们只需要在想要解决跨域的 Controller
层请求处理方法上标注 @CrossOrigin
注解即可。
@GetMapping("/{id}/{username}")
@ResponseBody
@CrossOrigin
public ResponseResult<User> getUserOneInfoByIdAndUsername(@PathVariable("id") Integer id, @PathVariable("username") String username){
User user = new User();
user.setId(id);
user.setName(username);
return ResponseResult.setCommonStatusAndData(ResultCode.SUCCESS,user);
}
当前端向服务器发送请求时 localhost:63343/user/1/yanghi
,我们通过浏览器查看请求信息:
会发现在请求头上加上了一个 Origin
请求头,表示从那个地址访问服务器:
然后服务器收到这个请求的访问后,就会判断是否支持访问,如果支持的话,就会在响应体上带上 Access-Control-Allow-Origin
表示支持跨域请求访问,*
表示允许所有域名的脚本访问该资源。
当然这个注解可以标注在类上,表示这个类的请求处理方法都允许所有跨域的脚本访问。
❝
@CrossOrigin
注解详解❞

这是 @CrossOrigin
注解的属性:
-
value,origins
用于指定允许跨域的路径(String[]
);如果允许所有的话可以写/**
,也可以不填;如果设置有限个路径可以这样写@CrossOrigin({"http://k1.com","http://k2com"})
-
originPatterns
用于指定允许跨域的路径(String[]
),与value,origins
功能相同。不过其支持通配符的形式配置路径。如https://*.domain1.com
,可以代表https://my.domain1.com
-
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
,在缓存时间内同一请求不需要“预检”请求。
-
使用 WebMvcConfigurer
的配置类解决
前面使用注解的方式,虽然我们可以在类上加上 @CrossOrigin
注解,允许跨域,但是我们 Controller
类不止一个类,所以使用配置的方式更为合适。通过使用 WebMvcConfigurer
的 addCorsMappings
方法配置 CorsInterceptor
进行解决。
首先我们在项目包下创建 config
包,所有的配置都在这个包下完成。然后创建 MySpringConfiguration
类
通过配置类的方式,书写一个方法,返回 WebMvcConfigurer
对象到 spring
容器中。
@Configuration(proxyBeanMethods = false)
public class MySpringConfiguration {
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer(){
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**") //设置允许跨域的路径 /**表示所有
.allowedOrigins("*") //设置允许跨域请求的域名 /*表示所有
.allowCredentials(true) //是否允许Cookie
.allowedMethods("GET", "POST", "DELETE", "PUT") //设置允许的请求方式
.allowedHeaders("*") //设置允许的header属性 *所有
.maxAge(3600); //设置跨域允许时间
}
};
}
}
实际开发中更倾向于这样写,因为上述的写法不太直观:
这个写法可以针对一个单独方面的配置进行修改,上述的写法针对全部的 spring
配置。
@Configuration //标注了@Configuration的类会自动注入到Spring容器中
public class MyMvcConfiguration implements WebMvcConfigurer{
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**") //设置允许跨域的路径 /**表示所有
.allowedOrigins("*") //设置允许跨域请求的域名 /*表示所有
.allowCredentials(true) //是否允许Cookie
.allowedMethods("GET", "POST", "DELETE", "PUT") //设置允许的请求方式
.allowedHeaders("*") //设置允许的header属性 *所有
.maxAge(3600); //设置跨域允许时间
}
}
建议指定的有限个源地址可以跨域访问资源,更加安全。
allowedOrigins
方法允许指定多个参数,只需要填入我们指定的域名就可以了。

3. CORS解决跨域的原理
3.1. 跨域请求的思路
CORS
解决跨域请求的思路是:如果浏览器要跨域访问服务器的资源,需要获得服务器的允许。

我们知道请求中附带很多信息,从而对服务器造成不同程度的影响。比如有的请求只是获取服务的资源(GET),有的请求会改动服务器的数据,这也说明了一点,为什么我们的请求方式不能只使用 GET
了。
针对不同的请求,CORS
规定了三种不同的交互模式,分别是:
-
简单请求 -
需要预检的请求 -
附带身份凭证的请求
这三种模式从上到下层层递进,请求可以做的事越来越多,要求也越来越严格。
3.2. 三种不同的交互模式的判定方式
-
简单请求:当请求「同时满足」以下条件时,浏览器会认为它是一个简单请求:
-
请求方式为 GET、POST、HEAD
的其中之一。 -
请求头中「仅」包含安全的字段,常见的安全字段有: Accept Accept-Language Content-Language Content-Type DPR Downlink Save-Data Viewport-Width Width
。 -
请求头如果包含 Content-Type
,仅限这些值text/plain multipart/form-data application/x-www-form-urlencoded
其中之一。 -
附带身份凭证的请求:默认情况下,跨域请求并不会附带
Cookie
,那么某些需要权限的操作就无法进行,不过可以通过简单的配置就可以实现附带Cookie
。
假如说我们使用的是 axios
请求库:那么需要在请求配置中设置 withCredentials: true
// `withCredentials` 表示跨域请求时是否需要使用凭证
withCredentials: true, // default false
-
需要预检的请求:不是简单请求也不是附带身份凭证的请求。
3.3. 三种不同交互模式中浏览器与服务端的交互流程

-
简单请求:当浏览器判定前端发送的跨域请求符合简单请求规范,就进行以下操作。
首先我们发送一个请求:
axios('www.yanghi.xyz/user/12345');
请求发出后,请求头的格式如下:
GET /user/12345/ HTTP/1.1
Host: crossdomain.com
Connection: keep-alive
...
Referer: www.yanghi.xyz
Origin: www.yanghi.xyz
浏览器进行检查,发现是跨域请求,就携带 Origin
字段会告诉服务器,是那个源地址在发送跨域请求。
然后服务收到请求后,如果允许请求跨域访问,就在响应头中添加 Access-Control-Allow-Origin
字段:当字段值为 *
,表示允许任何域名的请求;当字段值为具体的源是 www.yanghi.xyz
,表示只接收该源的请求。
HTTP/1.1 200 OK
Date: Tue, 21 Apr 2020 08:03:35 GMT
...
Access-Control-Allow-Origin: www.yanghi.xyz
...
消息体中的数据
然后浏览器比对服务器的响应头中的字段Access-Control-Allow-Origin
检查字段值是否与请求地址相同,或者是允许全部请求源。如果相同就把响应结果返回给请求,如果不同就报错(不允许跨域请求)。
-
需要预检的请求:
假如说我们发送了一个
POST
请求,并自定义 Content-type: application/json
。这就不符合简单请求的要求了,由于没有允许附带凭证,就是需要预检的请求了。
然后浏览器依照 CORS
进行检查,发现是需要预检的请求。
那么就会发送一个预检请求:OPTIONS
请求
OPTIONS /api/user HTTP/1.1
Host: crossdomain.com
...
Origin: www.yanghi.xyz
Access-Control-Request-Method: POST
Access-Control-Request-Headers: a, b, content-type
可以看出,这并非我们想要发出的真实请求,请求中不包含我们的响应头,也没有消息体。
这是一个预检请求,它的目的是询问服务器,是否允许后续的真实请求。
预检请求「没有请求体」,它包含了后续真实请求要做的事情
❝
预检请求的特征:
❞
-
请求方法为 OPTIONS
-
没有请求体 -
请求头中包含: -
Origin
:请求的源,和简单请求的含义一致 -
Access-Control-Request-Method
:后续的真实请求将使用的请求方法 -
Access-Control-Request-Headers
:后续的真实请求会改动的请求头
服务器收到预检请求后,不进行处理,响应下面的消息格式给浏览器:
HTTP/1.1 200 OK
Date: Tue, 21 Apr 2020 08:03:35 GMT
...
Access-Control-Allow-Origin: www.yanghi.xyz
Access-Control-Allow-Methods: POST
Access-Control-Allow-Headers: a, b, content-type
Access-Control-Max-Age: 86400
...
对于预检请求,不需要响应任何的消息体,只需要在响应头中添加:
-
Access-Control-Allow-Origin
:和简单请求一样,表示允许的源 -
Access-Control-Allow-Methods
:表示允许的后续真实的请求方法 -
Access-Control-Allow-Headers
:表示允许改动的请求头 -
Access-Control-Max-Age
:告诉浏览器,多少秒内,对于同样的请求源、方法、头,都不需要再发送预检请求了
然后浏览器发送真实请求:和简单请求一致,预检请求目的在于验证。
-
附带身份凭证的请求
首先浏览器会判断是简单请求还是预检请求。然后流程与上述两种请求流程一致。不同的是响应头中包含了 Access-Control-Allow-Credentials: true
,然后浏览器进行判断,为 true
则允许这个请求,否则报错。
3.4. 疑问?
❝
疑问:跨域请求的检查,是在服务端检查还是浏览器端检查?
❞
浏览器端检查,跨域资源共享( CORS
)是一种机制,是 W3C
标准。它允许浏览器向跨源服务器,发出 XMLHttpRequest
或 Fetch
请求。并且整个 CORS
通信过程都是浏览器自动完成的,不需要用户参与。
❝
为什么简单请求不需要预检?
❞
因为简单请求虽然是一种定义,不过它定义是有一定理由的,浏览器可能觉得这类请求预检的安全性没有那么大必要,不预检带来性能方面收益更大。因为预检的话,需要需要额外的请求,但你前后端项目中,如果跨域的都发送预检请求,是非常影响性能的。
❝
如何减少
CORS
预请求的次数?❞
服务端设置 Access-Control-Max-Age
字段(也就是我们配置中的 maxAge
属性),在有效时间内浏览器无需再为同一个请求发送预检请求。但是它有局限性:只能为同一个请求缓存,无法针对整个域或者模糊匹配 URL
做缓存。
「注意」
在跨域访问时,JS只能拿到一些最基本的响应头,如:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma
,如果要访问其他头,则需要服务器设置本响应头。Access-Control-Expose-Headers
头让服务器把允许浏览器访问的头放入白名单,例如:
Access-Control-Expose-Headers: authorization, a, b
4. 总结
通过对 CORS
原理分析,相信大家已经明白了为什么服务端允许跨域需要设置一些属性
这实际上是浏览器端和服务器端之间允许跨域所需要的规范,满足这些规范才能进行跨域请求。
原文始发于微信公众号(yanghi):前后端分离项目中的跨域问题解决及其原理分析
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之家整理,本文链接:https://www.bmabk.com/index.php/post/226744.html