前后端分离项目中的跨域问题解决及其原理分析

前言

由于前后端分离项目是两个项目,一个前端一个后端,各司其职。不同于服务端渲染项目。往往这两个项目部署在不同的服务器上,即使在同一台服务器下,端口号也是不同的,这就产生了跨域问题。

前后端分离项目中的跨域问题解决及其原理分析本篇文章主要介绍如何解决跨域以及跨域的原理分析,希望观众老爷们多多支持,请在评论区批评指正!

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/userhttps://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 解决跨域」

  1. 使用 @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,在缓存时间内同一请求不需要“预检”请求。

  1. 使用 WebMvcConfigurer 的配置类解决

前面使用注解的方式,虽然我们可以在类上加上 @CrossOrigin 注解,允许跨域,但是我们 Controller 类不止一个类,所以使用配置的方式更为合适。通过使用 WebMvcConfigureraddCorsMappings 方法配置 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规定了三种不同的交互模式,分别是:

  1. 简单请求
  2. 需要预检的请求
  3. 附带身份凭证的请求

这三种模式从上到下层层递进,请求可以做的事越来越多,要求也越来越严格。

3.2. 三种不同的交互模式的判定方式

  1. 简单请求:当请求「同时满足」以下条件时,浏览器会认为它是一个简单请求:

    • 请求方式为 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 其中之一。
  2. 附带身份凭证的请求:默认情况下,跨域请求并不会附带 Cookie ,那么某些需要权限的操作就无法进行,不过可以通过简单的配置就可以实现附带 Cookie

假如说我们使用的是 axios请求库:那么需要在请求配置中设置 withCredentials: true

// `withCredentials` 表示跨域请求时是否需要使用凭证
withCredentials: true, // default false
  1. 需要预检的请求:不是简单请求也不是附带身份凭证的请求。

3.3. 三种不同交互模式中浏览器与服务端的交互流程

前后端分离项目中的跨域问题解决及其原理分析
  1. 简单请求:当浏览器判定前端发送的跨域请求符合简单请求规范,就进行以下操作。

首先我们发送一个请求:

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检查字段值是否与请求地址相同,或者是允许全部请求源。如果相同就把响应结果返回给请求,如果不同就报错(不允许跨域请求)。


  1. 需要预检的请求:

前后端分离项目中的跨域问题解决及其原理分析假如说我们发送了一个 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:告诉浏览器,多少秒内,对于同样的请求源、方法、头,都不需要再发送预检请求了

然后浏览器发送真实请求:和简单请求一致,预检请求目的在于验证。


  1. 附带身份凭证的请求

首先浏览器会判断是简单请求还是预检请求。然后流程与上述两种请求流程一致。不同的是响应头中包含了 Access-Control-Allow-Credentials: true ,然后浏览器进行判断,为 true 则允许这个请求,否则报错。

3.4. 疑问?

疑问:跨域请求的检查,是在服务端检查还是浏览器端检查?

浏览器端检查,跨域资源共享( CORS )是一种机制,是 W3C 标准。它允许浏览器向跨源服务器,发出 XMLHttpRequestFetch 请求。并且整个 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

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之家——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!