持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第6天,点击查看活动详情
前言
由于前后端分离项目是两个项目,一个前端一个后端,各司其职。不同于服务端渲染项目。往往这两个项目部署在不同的服务器上,即使在同一台服务器下,端口号也是不同的,这就产生了跨域问题。
本篇文章主要介绍如何解决跨域以及跨域的原理分析,希望观众老爷们多多支持,请在评论区批评指正!
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中配置反向代理。上述的代理只用于开发环境。
2.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.comallowedHeaders允许跨域的请求头信息,默认为“*”表示允许所有的请求头,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 原理分析,相信大家已经明白了为什么服务端允许跨域需要设置一些属性
这实际上是浏览器端和服务器端之间允许跨域所需要的规范,满足这些规范才能进行跨域请求。