网络通信中的安保问题——浏览器同源策略、跨域问题

138 阅读7分钟

同源策略是一套浏览器的安全机制,当一个的文档和脚本,与另一个的资源进行通信时,同源策略就会对这个通信做出不同程度的限制。
而由此限制造成的开发问题,称之为跨域(异源)问题。

用形象的比喻同源策略,就很像你家小区的保安,当小区外部的人员想进小区去你家时,保安就会将这个人拦下来进行询问或登记。怎么让保安来放行,这就是解决跨域问题。

同源和异源

源(origin)= 协议 + 主机 + 端口

image.png

如图,https://juejin.cn:443/news/details?id=talia#main 的源即为 https://juejin.cn:443

在网页中进行通信的资源有很多,如 css、js、image 等,同源策略都会进行限制,而其中限制最狠的当属 ajax,默认情况下,完全不允许 ajax 访问跨域资源。

css/js/image 等资源就好比你的亲朋好友,可能会和你家小区保安混个脸熟,而 ajax 就是一脸凶相的陌生人,保安当然加倍警惕了。那怎么让保安知道这次来访的人是自己人呢,需要以下几种方法。

解决跨域问题

共有三个方法可以解决:代理(主流方法)、CORS、JSONP(不建议使用)。

一、代理

对于前端而言,大部分的跨域问题都是通过代理解决。

适用场景:生产环境不发生跨域,开发环境发生跨域

所以只需要在开发环境解决跨域即可,这种代理也称之为开发代理

image.png

看图可知,开发环境下,由前端开发服务器如 dev-server,和后端开发服务器进行通信,而前端开发服务器是自己人,所以不需要保证目标服务器是自己人。

前端开发服务器在 webpack 或 vite 配置文件中进行代理的相关配置。

生产环境下,没有前端开发服务器,而是用代理服务器如 nginx、Apache 等进行代理转发。

这个方案就比如要去你家送东西的陌生人,他正好认识你家小区的物业人员,他委托这个物业人员作为代理人,把东西送到你家,因为物业人员和保安都是自己人,当然不会被拦截了。

二、CORS

CORS是基于http1.1的一种跨域解决方案,它的全称是 Cross Origin Resource Sharing,跨域资源共享。

它的总体思路是:只要服务器明确表示允许,则校验通过;若拒绝或无任何表示,则不通过。

所以用 CORS 解决跨域,必须保证服务器是自己人。而有些请求只是获取服务器数据,有些请求是要修改服务器数据,针对不同安全等级的请求,CORS 规定了不同的请求类型:

  • 简单请求
  • 预检请求
  • 附带身份凭证的预检请求

简单请求

简单请求需全部满足下列三个条件:

  1. 请求方法是 GET、POST、HEAD 之一。
  2. 请求头仅包含安全字段,浏览器默认请求头都是安全的,详见 W3C CORS 安全规范。
  3. 如果请求头有 Content-Type,则值必须是:
    • text/plain
    • multipart/form-data
    • application/x-www-form-urlencoded

简单请求解决跨域的方法:

当浏览器识别到此 ajax 请求属于简单请求后,在请求头中会自动加上请求源字段 Origin,值为发起请求的源。

当服务器收到请求后,如果允许该请求跨域访问,
则需要在响应头中添加 Access-Control-Allow-Origin 字段,值可以为:
*(表示允许全部请求源访问,不安全)
具体的源(比如当前客户端发请求的源)

就比如要去你家做客的亲戚,你提前在物管 app 上录入了他的人脸信息,他进小区时扫脸匹配成功了,就可以进入了。

预检请求

简单请求之外的都是预检请求。因为简单请求对服务器而已威胁不大,所以简单配置即可,而预检请求则需要以下流程:

  1. 浏览器发送预检请求,询问服务器是否允许
  2. 服务器允许
  3. 浏览器发送真实请求
  4. 服务器完成真实响应

预检请求解决跨域的方法:

  1. 浏览器先自动发送同名的预检请求 OPTIONS,询问服务器是否允许。

image.png

由图可见,预检请求没有请求体,
Origin 就是真实请求的源,
Access-Control-Request-Method 是后续真实请求的请求方法,
Access-Control-Request-Headers 是后续真实请求所携带的自定义请求头。

2.服务器会根据预检请求中提供的这些信息,来检查是否允许跨域请求,如果允许,则需响应下面的消息格式:

image.png

由图可见,对于预检请求,服务器不需要响应体,
Access-Control-Allow-Origin 就是允许请求的源,
Access-Control-Allow-Methods 是允许后续真实请求的请求方法,
Access-Control-Allow-Headers 是允许后续真实请求所携带的自定义请求头,
Access-Control-Max-Age 是允许浏览器在 3600 秒内,对于同样的请求源、请求方法、请求头都不需要再发送预检请求了,节省资源。

  1. 通过之后浏览器才发送真实请求,同简单请求,请求携带 Origin。
  2. 服务器响应真实请求,同简单请求,响应携带 Access-Control-Allow-Origin。

就比如给你送快递的小哥,保安会给你打个电话预先确认一下,如果你同意了,那么快递小哥会被放行,由于你经常要这个快递小哥给你送东西,你就在物管 app 上把他的信息录入了一个临时进出凭证,并设置了一周内有效,接下来的一周只要是这个快递小哥,保安都不会拦截了。

附带身份凭证的预检请求

默认情况下,ajax 的跨域请求不会携带 cookie,某些需要权限的操作就无法进行。

这时需要对请求进行简单配置,实现一个附带凭证的请求。

// fetch api
fetch(url, {
    credentials: 'include' //凭证:包含
})

加上此配置后,无论是简单请求还是预检请求,都会在请求头中添加 cookie 字段。

而服务器若允许此凭证,需在响应头添加 Access-Control-Allow-Credentials: true,若不添加则任视为跨域请求被拒绝。

注:对于附带身份凭证的预检请求,规定服务器不得设置 Access-Control-Allow-Origin 的值为 *。

关于跨域情况下获取响应头:
在跨域访问时,js 只能拿到一些基本的响应头,如果需要访问其他响应头,则需要服务器设置该响应头。
Access-Control-Expose-Headers 字段允许服务器把浏览器访问的响应头放入白名单。
如:Access-Control-Expose-Headers: authorization, customHeader1, customHeader2

三、JSONP

老版本浏览器不支持 CORS 方案,所以采用 JSONP 方法实现跨域。

其原理是利用浏览器对 script 标签的低验证等级特性,配合服务器的某些操作来获取响应,所以要求服务器也必须是自己人。

JSONP解决跨域的方法:

客户端不使用 ajax 发送请求,而是生成一个 script 标签,src 是要访问的服务器地址,当浏览器解析到该标签时会直接向服务器发送请求(因为低验证等级特性),同时客户端准备好一个固定名称的回调函数
服务器对所请求的地址响应一段 js 代码,代码内容就是客户端准备好的固定名称的函数调用,函数入参就是服务器要响应的数据,即响应体内容。

image.png

JSONP 虽然可以解决跨域问题,但也有明显的缺陷:

  • 仅支持 GET 请求
  • 容易产生 XSS 攻击,如修改该回调函数实现
  • 容易被恶意调用,因为不对源进行验证

这个方案就好比你家是老小区,没有物管 app 也没有人脸识别,你私下把你的车子借给小王,当小王来到你家小区时,保安一看是你的车子以为小王是你家亲戚,然后放行。所以如果有个坏人偷了你的车子,那么也可以随便进入你的小区了,这就太不安全了。