Skip to content
On this page

再学跨域(二)-- CORS - 跨域资源共享

关于博主:前端程序员,最近专注于 webGis 开发。加微信:MasonChou123,进技术交流群。

本文是再学跨域系列的第二篇,主要介绍 CORS 跨域解决方案。

浏览器的同源策略限制了不同源或者不同网站之间的交互,但是在实际开发中,需要需要请求不同源的数据,比如前端应用需在网站 A 中通过 AJAX 请求网站 B 的数据,比如地图应用从 C 网站加载图片写入到 A 网站的 canvas 中,同源策略就阻碍了这种系统集成方式。

CORS(Cross-Origin Resource Sharing)是 W3C 标准,是跨域资源共享的缩写,是一种跨域解决方案,或者说不同域的系统集成方案。

1. AJAX 跨域请求发生了什么?

在介绍同源限制的文章中,使用表单提交的数据是可跨域的:

html
<form action="http://localhost:5566/form-post" method="post" accept-charset="utf-8" target="targetIfr">
  <input type="text" name="age" />
  <button>提交</button>
</form>
<form action="http://localhost:5566/form-get" method="get" target="targetIfr">
  <input type="text" name="name" />
  <button>提交</button>
</form>

post 请求的关键信息:

bash
POST /form-post HTTP/1.1 # method path 协议
Host: localhost:5566 # 目的主机和端口
Origin: http://localhost:3000 # 请求的源
Referer: http://localhost:3000/ # 请求的来源
Sec-Fetch-Dest: iframe # 请求的目标
Sec-Fetch-Mode: navigate # 请求的模式

先认识几个新的请求头信息

Sec-Fetch-Dest 告诉服务器请求的资源类型,以便让服务做出个更精确的响应。

它的值有:

  • audio 音频数据
  • document 文档
  • empty 空 fetch、XMLHttpRequest、WebSocket、EventSource 等发起请求
  • font 字体 @font-face 发起。
  • frame iframe 发起。
  • iframe iframe 发起。
  • image 图片 <image>、SVG、CSS background-image、CSS cursor、CSS list-style-image 等。
  • manifest link rel="manifest" 发起。
  • script 脚本 <script src>发起。
  • style 样式 <link rel="stylesheet> 或者 @import 发起。
  • video 视频数据 <video> 发起。

Sec-Fetch-Mode ,表示请求模式,它的值有:

含义描述
cors跨域请求服务器端需要设置响应头 Access-Control-Allow-Origin
same-origin同源请求
navigate导航请求比如点击链接、重定向、form 提交等,服务返回的是一个新的页面
no-cors跨域请求,无需设置跨域响应头但是不需要服务器端设置响应头 Access-Control-Allow-Origin ,默认是 no-cors
websocketwebsocket 请求

no-cors 表示跨域了,不需要服务器设置跨域响应头。什么情况下会是这种模式呢? 图片脚本样式表 等允许跨域且不用设置跨域响应头的资源。

Sec-Fetch-Mode 和 fetch 的 mode 属性意义和值都是一样的。

Sec-Fetch-Site 表示请求的站点类型,它的值有:

含义
cross-site跨站点请求
same-origin同源请求
same-site同站点请求
none用户触发页面导航

same-site: 同站点请求,是否是同站点,判断规则复杂。

同站判断规则 -- Sec-Fetch-*请求头,了解下?

Sec-Fetch-User 表示请求的用户类型,它的值有:

含义
?1导航请求由用户激活触发,鼠标点击、按键
?0导航请求由用户激活触发以外的情况触发

请求头只会在导航请求情况下携带,导航请求包括 document , embed , frame , iframe , or object。

只要包含前缀 Sec- 都属于应用程序禁止修改的 HTTP 消息头,用户代理保留全部对它们的控制权,它们是请求元信息,服务器能根据这些信息做出更精确的响应,比如选择更加适合的资源,或者拒绝请求,提高安全性。

origin vs referer

Referer 和 Origin 都是 HTTP 请求头,用于向服务器提供关于请求来源的信息,但它们之间存在一些重要的区别:

Referer

  • 作用: 指示当前请求是从哪个页面链接过来的。
  • 包含信息: 包括协议、域名、路径、查询参数等,但不包括锚点(#)和用户信息(如用户名: 密码)。
  • 发送时机: 几乎所有请求都会发送 Referer 头,除非浏览器无法获取到请求源。
  • 用途:
    • 日志分析: 帮助网站管理员分析用户行为,了解用户是如何访问到当前页面的。
    • 防盗链: 网站可以通过检查 Referer 头来判断图片、视频等资源是否被非法引用。
    • 搜索引擎优化: 搜索引擎可以通过 Referer 头来判断页面之间的链接关系,从而更好地理解网页内容。

Origin

  • 作用: 指示请求的来源站点。
  • 包含信息: 只包含协议、域名和端口号。
  • 发送时机: 只有在跨域请求或者同源下 GET、HEAD 以外的请求方法时才会发送 Origin 头。
  • 用途:
    • 跨域资源共享(CORS): 服务器通过检查 Origin 头来判断是否允许跨域请求。
    • 安全: 帮助服务器防止跨站请求伪造(CSRF)攻击。

Origin 的值可以是 null,请求来源是隐私的:

  • 跨源的图像或媒体,包括:img、video 和 audio 元素。
  • 请求来源的协议不是 http、https、ftp、ws、wss 或 gopher 中的任意一个(如:blob、file 和 data)。

cors 和 cross-site 有什么不同

CORS(Cross-Origin Resource Sharing)

  • 跨域资源共享: 是一种机制,它使用额外的 HTTP 头来允许浏览器发起跨域 HTTP 请求。
  • 作用: 主要是为了解决浏览器同源策略的限制,允许前端 JavaScript 代码向不同域名下的服务器发起请求。
  • 实现方式: 通过在服务器端设置相应的 HTTP 响应头来实现。浏览器在发起跨域请求时,会检查服务器返回的响应头,如果符合 CORS 规范,则允许请求。

cross-site

  • 更广义的概念: 包括了 CORS,还包括其他类型的跨站行为,比如跨站脚本攻击(XSS)、跨站请求伪造(CSRF)等。

  • 含义: 指的是在不同的网站或域之间进行的操作。

  • 分类:

    • 跨站脚本攻击(XSS): 攻击者通过注入恶意脚本到网页,从而在用户的浏览器上执行这些脚本。
    • 跨站请求伪造(CSRF): 攻击者诱导已登录的用户点击一个恶意的链接,从而在不知情的情况下以用户的名义发送请求,执行一些非法的操作。
  • CORS 是一个技术术语,指的是一种机制,用于解决浏览器同源策略的限制。

  • cross-site 是一个更广义的概念,指的是在不同的网站或域之间进行的操作,它包含了 CORS 以及其他类型的跨站攻击。

使用 AJAX 发送相同的请求

使用 fetch 发送相同的请求:

js
/**
 * @Author: ZhouQiJun
 * @Description: 关于博主,前端程序员,最近专注于 webGis 开发
 * @加微信: MasonChou123,进技术交流群
 */
function sendSameHttp() {
  const url = 'http://localhost:5566/form-post'
  fetch(url, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
      },
      body: 'age=123',
    })
    .then(res => {
      console.log('res', res)
      return res.json()
    })
    .then(data => {
      console.log('data', data)
    })
    .catch(err => {
      console.log('err', err)
    })
}

报错了:

bash
Access to fetch at 'http://localhost:5566/form-post' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

报错信息大概的意思是: 请求 http://localhost:5566/form-posthttp://localhost:3000 的 CORS 策略拦截了,因为请求头中没有 Access-Control-Allow-Origin 响应头,如果你需要一个不透明的响应,可以设置请求模式为 no-cors

按照提示设置请求模式为 no-cors

js
/**
 * @Author: ZhouQiJun
 * @Description: 关于博主,前端程序员,最近专注于 webGis 开发
 * @加微信: MasonChou123,进技术交流群
 */
function sendSameHttp() {
  const url = 'http://localhost:5566/form-post'
  fetch(url, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
      },
      body: 'age=123',
      mode: 'no-cors'
    })
    .then(res => {
      console.log('res', res)
      return res.json()
    })
    .then(data => {
      console.log('data', data)
    })
    .catch(err => {
      console.log('err', err)
    })
}

不报错了,但是拿不到返回数据:

js
{
  body: null,
  ok: false,
  url: '',
  status: 0,
  statusText: '',
  type: 'opaque'
}

no-cors 没有实现跨域请求的目标,它表示开发明确知道请求跨域了,但是我不需要拿到请求资源。这个只极少使用。

服务器给出的数据去哪儿了? 被浏览器拦截了。

2. CORS 跨域解决方案

实现跨域的关键在于服务器,需要服务器配置请求头,告知浏览器是否允许客户端读取 http 返回值。它建立在 http 标准之上。

现在开启服务器支持跨域:

js
app.use(function(req, res, next) {
  res.header('Access-Control-Allow-Origin', 'http://localhost:3000') // 第二个参数可以换成你的域名
  next()
})

Access-Control-Allow-Origin 的值是允许跨域的域名,只能是一个值或者 * ,表示允许所有域名访问。

不能使用逗号分隔的多个值,否则报错:

bash
The 'Access-Control-Allow-Origin' header contains multiple values 'http://localhost:3000,http://localhost:3001', but only one is allowed.

希望跨域发送 json 数据,把请求改成:

js
/**
 * @Author: ZhouQiJun
 * @Description: 关于博主,前端程序员,最近专注于 webGis 开发
 * @加微信: MasonChou123,进技术交流群
 */
function sendSameHttp() {
  const url = 'http://localhost:5566/form-post'
  fetch(url, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        name: 'zqj',
        age: 18,
      }),
    })
    .then(res => {
      console.log('res', res)
      return res.json()
    })
    .then(data => {
      console.log('data', data)
    })
    .catch(err => {
      console.log('err', err)
    })
}

浏览器多了一个 OPTIONS 请求,且报错了:

bash
Access to fetch at 'http://localhost:5566/form-post' from origin 'http://localhost:3000' has been blocked by CORS policy: Request header field content-type is not allowed by Access-Control-Allow-Headers in preflight response.

大概意思是请求头字段 content-type 在预检请求头不被允许。

在介绍预检请求之前,先说下跨域请求的分类: 简单请求和非简单请求。

2.1 简单请求

简单请求:能通过 form 表单发起的请求,这种跨域请求不需要预先发送一个 OPTIONS 请求去询问服务器是否有权限。

具体来说,简单请求需要满足三个条件:

  • 请求方法:GET 、POST、HEAD
  • 请求头只能包含四个:content-type、accept、accept-language
  • content-type 只能有三个:application/x-www-form-urlencoded,multipart/form-data 或 text/plain。

2.2 非简单请求

不是简单请求,就是非简单请求,常见的简单请求:

  • 发送 json: content-type 为 application/json
  • PUT、DELETE 等复杂方法。

实践中抓住这两点就能迅速判断,因为项目中通常是 json 进行前后端交互。

2.3 预检请求

非简单请求跨域时,才需要先发起预检请求。

2.4 为何有预检请求?

两个原因:

  • 兼容性:互联发展初期,人们并没有预料到后来需要 js、XMLHttpRequest 和跨域,都是使用和服务器交互都是使用 form 表单,form 发起的请求都是简单请求。制定 CORS 后,需要保证老的网站不会因为新标准而无法工作。

  • 安全性:这是 CORS 标准的关键,就是预检请求询问服务器是否允许复杂请求跨域的。如果没有复杂请求,完全可使用 form 实现跨域。

服务器设置允许跨域的请求头:

js
app.use(function(req, res, next) {
  res.header('Access-Control-Allow-Origin', 'http://localhost:3000') // 第二个参数可以换成你的域名
  res.header('Access-Control-Allow-Headers', 'Content-Type')
  next()
})

现在发送 json,不报错了。

Access-Control-Allow-Headers 告知浏览器,允许跨域的请求头。跨域请求包含复杂的 content-type 类型或者自定义请求头,就要配置这个响应头。

希望在接口中添加自定义请求头 api-version 以表明接口版本:

js
{
  headers: {
    'content-type': 'application/json',
    'api-version': '1.2.0'
  }
}

再次发起请求,报错:

bash
Request header field api-version is not allowed by Access-Control-Allow-Headers in preflight response.

大致意思是请求字段 apo-version 预检请求的 Access-Control-Allow-Headers 中。

现在加上:

js
app.use(function(req, res, next) {
  res.header('Access-Control-Allow-Origin', 'http://localhost:3000') // 第二个参数可以换成你的域名
  res.header('Access-Control-Allow-Headers', 'content-type,Api-version')
  next()
})

跨域成功了。

Access-Control-Allow-Methods ,开启允许跨域的方法,默认是 GETPOSTHEAD , 和 form 表单能使用的方法兼容。

Access-Control-Allow-Headers 可设置为 * ,表示不允许携带自定义请求头。

2.5 如何携带认证信息?

想要跨域请求中,发送认证信息,比如 cookie ,如何办?

fetch 携带 cookie,设置 credentialsinclude

fetch credentials 值有三个:

含义
same-origin同源才发
omit不发
include跨域也发

认证信息有哪些:cookie 、HTTP Basic authentication。

携带了认证信息,报错了:

bash
Response to preflight request doesn't pass access control check: The value of the 'Access-Control-Allow-Credentials' header in the response is '' which must be 'true' when the request's credentials mode is 'include'.

需要再服务端设置 Access-Control-Allow-Credentials 为 true,允许请求携带 cookie。

js
res.header('Access-Control-Allow-Credentials', true)

如果希望通过 http basic authentication 发送认证信息,需要在请求头中添加 Authorization 字段。

js
res.header('Access-Control-Allow-Headers', 'content-type,Api-version,Authorization')

需要携带 cookie,服务端设置 Access-Control-Allow-Origin 不能为 * ,必须是具体的域名。

2.6 预检请求的缓存

预检请求是有缓存的,浏览器会缓存预检请求的结果,下次再发起相同的请求时,就不会再发起预检请求了。

预检请求的缓存时间是多久呢? 规范是 5 秒,可以通过设置 Access-Control-Max-Age 来设置缓存时间。

js
res.header('Access-Control-Max-Age', 60 * 60 * 6) // 6 小时

2.7 如何获取响应的头信息?

服务器需要设置允许前端获取的响应头:

js
res.header('Access-Control-Expose-Headers', 'hello')

默认允许获取的响应头是: content-type、content-length。

2.8 如何处理多个域名跨域?

access-control-allow-origin 只能设置一个值,如果需要多个域名跨域,可以通过判断请求头中的 origin 来设置。

js
/**
 * @Author: ZhouQiJun
 * @Description: 关于博主,前端程序员,最近专注于 webGis 开发
 * @加微信: MasonChou123,进技术交流群
 */
const whiteList = ['http://localhost:3000', 'http://localhost:3001']

app.use(function(req, res, next) {
  const origin = req.headers.origin
  if (whiteList.includes(origin)) {
    res.header('Access-Control-Allow-Origin', origin)
  }
  next()
})

到了这里,CORS 跨域解决方案就介绍完了。

浏览器如何检查跨域是否合法的?

使用伪代码来描述:

js
// ACAO 是 Access-Control-Allow-Origin
// ACAC 是 Access-Control-Allow-Credentials
// credentials mode 是 CM
if (ACAO === null) return false
if (ACAO === '*' && CM !== 'include') return true
if (request.origin !== ACAO) return false
if (CM !== 'include') return true
if (ACAC === true) return true
return false

如何检验对 CORS 的理解?

  • 简单请求和非简单请求的区别 ✅
  • 根据服务器返回的响应头判断是否允许跨域、是否可以携带 cookie 和前端能保持哪些请求头 ✅
  • 从跨域错误中分析出问题 ✅

两个调试技巧

  • 不需要登录凭证,可使用浏览器直接访问资源,然后分析响应头,比如跨域图片、音频等资源。
  • 需要登录凭证,可使用 postman 发送请求,然后分析响应头。

日常开发中,特别关注 Access-Control-Allow-OriginAccess-Control-Allow-HeadersAccess-Control-Allow-MethodsAccess-Control-Allow-Credentials 这几个响应头。

参考资料

CORS 完全手册(三):CORS 详解

小结

CORS 是一种跨域解决方案,它是 W3C 标准,主要是通过设置响应头来告知浏览器是否允许跨域请求。跨域的关键在于服务器,但是作为前端工程师,必需了解跨域可能的问题,能快速定位问题

  • 简单请求和非简单请求的区别
  • 预检请求的作用
  • 如何设置允许跨域的请求头:Access-Control-Allow-Origin 指定允许跨域的域。
  • 如何设置允许跨域的请求头:Access-Control-Allow-Headers 指定允许跨域的请求头。
  • 如何设置允许跨域的请求头:Access-Control-Allow-Methods 指定允许跨域的方法。
  • 如何携带认证信息:设置 fetch 的 credentials 为 include,服务端设置 Access-Control-Allow-Credentials 为 true。
  • 预检请求的缓存:设置 Access-Control-Max-Age 缓存预检请求的结果。
  • 如何获取响应头:设置 Access-Control-Expose-Headers 允许前端获取的响应头。
  • 多个域名跨域:通过判断请求头中的 origin 来设置允许跨域的域。
  • no-cors 请求模式:不需要服务器设置跨域响应头,但是不需要服务器端设置响应头 Access-Control-Allow-Origin ,默认是 no-cors 。
  • 跨域最佳实践:设置 Access-Control-Allow-Origin 为具体的域名,不要使用 *,设置 content-type, 方便使用 json 传输数据。

关于博主,前端程序员,最近专注于 webGis 开发,加微信: MasonChou123,进技术交流群。

Released under the MIT License.