跨域问题及解决方案

Sun, July 28, 2024 - 9 min read
跨域简单请求示意图

同源策略及跨域问题

同源策略是一套浏览器的安全机制,它对同源资源放行,对跨域资源限制。

因此限制造成的开发问题,称之为跨域问题。

什么是同源

源 = 协议 + 域名 + 端口

同源即相同的协议、域名和端口号。

跨域出现的场景

  • 网络通信
    • a元素的跳转;加载CSS、JS、图片等;AJAX等等
  • JS API
    • window.open,window.parent,iframe

本文重点讨论网络通信中的Ajax跨域问题。

跨域解决方案

CORS(跨域资源共享)

跨源资源共享CORS,或通俗地译为跨域资源共享)是一种基于 HTTP 头的机制,该机制通过允许服务器标示除了它自己以外的其他(域、协议或端口),使得浏览器允许这些源访问加载自己的资源。跨源资源共享还通过一种机制来检查服务器是否会允许要发送的真实请求,该机制通过浏览器发起一个到服务器托管的跨源资源的“预检”请求。在预检中,浏览器发送的头中标示有 HTTP 方法和真实请求中会用到的头。

CORS的基本理念是:

  • 只要服务器明确表示允许,则校验通过
  • 服务器明确拒绝或没有表示,则校验不通过

所以,使用CORS解决跨域,必须要保证服务器是「自己人」

请求分类

CORS将请求分为两类:简单请求预检请求

对不同种类的请求它的规则有所区别。

所以要理解CORS,首先要理解它是如何划分请求的。

简单请求

完整判定逻辑:简单请求

简单来说,只要满足下列条件,就是简单请求:

  • 请求方法是GET、POST、HEAD之一
  • 头部字段满足CORS安全规范

浏览器请求默认自带的头部字段都是满足安全规范的,只要开发者不改动和新增头部,就不会打破此规则。

  • 如果有Content-Type,必须是下列值中的一个
    • text/plain
    • multipart/form-data
    • application/x-www-form-urlencoded

预检请求(preflight)

不是简单请求的就是预检请求,“需预检的请求”要求必须首先使用 OPTIONS 方法发起一个预检请求到服务器,以获知服务器是否允许该实际请求。“预检请求“的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响。

对于简单请求的验证

对于简单请求的验证

对于预检请求的验证

  1. 发送预检请求

发送预检请求

OPTIONS 预检请求中同时携带了下面两个标头字段:

Access-Control-Request-Method: POST
Access-Control-Request-Headers: a, b, Content-Type

标头字段 Access-Control-Request-Method 告知服务器,实际请求将使用 POST 方法。标头字段 Access-Control-Request-Headers 告知服务器,实际请求将携带三个自定义请求标头字段:a、b 与 Content-Type。服务器据此决定,该实际请求是否被允许。

服务器响应预检请求,表明服务器将接受后续的实际请求方法和实际请求头。

Access-Control-Allow-Origin: http://abc.com
Access-Control-Allow-Methods: POST
Access-Control-Allow-Headers: a, b, Content-Type
Access-Control-Max-Age: 86400

服务器的响应携带了 Access-Control-Allow-Origin: http://abc.com,从而限制请求的源域。同时,携带的 Access-Control-Allow-Methods 表明服务器允许客户端使用 POST 方法发起请求。

标头字段 Access-Control-Allow-Headers 表明服务器允许请求中携带字段 a、b 与 Content-Type。与 Access-Control-Allow-Methods 一样,Access-Control-Allow-Headers 的值为逗号分割的列表。

最后,标头字段 Access-Control-Max-Age 给定了该预检请求可供缓存的时间长短,单位为秒,默认值是 5 秒。在有效时间内,浏览器无须为同一请求再次发起预检请求。以上例子中,该响应的有效时间为 86400 秒,也就是 24 小时。请注意,浏览器自身维护了一个最大有效时间,如果该标头字段的值超过了最大有效时间,将不会生效。

预检请求完成之后,发送实际请求。

  1. 发送真实请求(和简单请求一致)

携带身份凭证的请求(cookie)

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

我们可以通过简单的配置来实现跨域请求附带cookie的效果:

// xhr
const xhr = new XMLHttpRequset()
xhr.withCredentials = true
 
// fetch
fetch(url, {
	credentails: "include"
})

这样,无论是简单请求还是预检请求,都会在请求头中添加cookie字段。

但是,如果服务器端的响应中未携带 Access-Control-Allow-Credentials: true浏览器不会把响应内容返回给请求的发送者。具体来说:

  • 如果请求是预检请求,那么在预检请求时不会包含凭据。如果服务器对预检请求的响应将 Access-Control-Allow-Credentials 标头设置为 true,则实际请求时将包含凭据;否则,浏览器将报告网络错误。
  • 如果请求时未经过预检,则请求将包含凭据;如果服务器的响应没有将 Access-Control-Allow-Credentials 标头设置为 true,浏览器将报告网络错误。

同时,对于附带身份凭证的请求,服务器:

  • 服务器不能将 Access-Control-Allow-Origin 的值设为通配符“*”,而应将其设置为特定的域,如:Access-Control-Allow-Origin: https://example.com。
  • 服务器不能将 Access-Control-Allow-Headers 的值设为通配符“*”,而应将其设置为标头名称的列表,如:Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
  • 服务器不能将 Access-Control-Allow-Methods 的值设为通配符“*”,而应将其设置为特定请求方法名称的列表,如:Access-Control-Allow-Methods: POST, GET

关于跨域获取响应头中的内容

通过在响应头中设置Access-Control-Expose-Headers

在跨源访问时,XMLHttpRequest 对象的 getResponseHeader() 方法(或者fetch中使用headers.get())只能拿到一些最基本的响应头,Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma,如果要访问其他头,则需要服务器设置该响应头。

JSONP

使用<script>标签发起跨域请求,响应回来的参数是一个callbackName({msg: 'xxxxx'})的形式。

缺陷:

  • 仅能使用GET请求
  • 容易产生安全隐患

恶意攻击者可能利用callback=恶意函数的方式实现XSS攻击

  • 容易被非法站点恶意调用

代理

CORS和JSONP均要求服务器是「自己人」

那如果不是呢?那就找一个「中间人」

配置代理服务器

如何选择

最重要的依据:保持生产环境和开发环境一致

选择跨域解决方案决策图:

跨域解决方案

具体使用场景:

  1. 生产环境请求静态资源服务器不跨域,请求数据服务器跨域

生产环境

开发环境

  1. 生产环境没有跨域

生产环境

开发环境