CORS

1,605 阅读7分钟

参考自:juejin.cn/post/686694…

本文仅供自己学习使用

什么是跨域

协议,域名,端口,三者有一个不一样,就是跨域。

如何解决跨域

  • CORS(Cross-Orign Resource Sharing),在服务器端设置几个响应头
  • Reverse Proxy,在nginx/traefik/haproxy等反向代理服务器中设置为同一域名。

CORS

CORS即跨域资源共享(Cross-Origin Resource Sharing,CORS)。简而言之,就是在服务器端的响应中加入几个标头,使得浏览器能够跨域访问资源。

这个响应头的字段设置就是 Access-Control-Allow-Origin: *,如下图所示

以下是一个最简单的CORS请求

GET / HTTP /1.1
Host: shanyue.tech
Origin: http://shanyue.tech
User-Agent:Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36 Edg/85.0.564.44

HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
Content-Type: text/plain; charset=utf-8
Content-Length: 12
Date: Wed, 02 Sep 2020 11:03:44 GMT
Connection: keep-alive

预请求与Options 当一个请求跨域且不是简单请求时就会发起预请求,也就是Options。如果没有预请求,万一有一个毁灭性的POST跨域请求直接执行,虽然最后告知浏览器你没有跨域权限,但是损失已造成.

以下条件构成了简单请求:

  • Method:请求的方法是GET、POST及HEAD
  • Header:请求头是Content-Type(有限制)、Accept-Language、Content-Language 等
  • Content-Type:请求类型是application/x-www-form-urlencoded、multipart/form-data或text/plain

非简单请求一般需要开发者主动构造,在项目常见的 Content-Type:application/json 及 Authorization:为典型的非简单请求。与之相关的三个字段如下:

  • Access-Control-Allow-Methods:请求所允许的方法,用于预请求(preflight request)中
  • Access-Control-Allow-Headers:请求所允许的头,用于预请求(preflight request)中
  • Access-Control-Max-Age:预请求的缓存时间

CORS Middleware

关于CORS的设置即是对CORS相关响应头的设置。

CORS的请求头有:

  • Access-Control-Allow-Origin:可以把资源共享给哪些域名,支持*及特定域名
  • Access-Control-Allow-Methods:请求是否可以带cookie
  • Access-Control-Allow-Headers:请求所允许的方法,用于预请求(preflight request)中
  • Access-Control-Allow-Credentials(请求所允许的头,用于预请求(preflight request)中
  • Access-Control-Expose-Headers:哪些头可以在响应中列出
  • Access-Control-Max-Age:预请求的缓存时间

而关于CORS的中间件既是使用默认值与配置来设置这些头,如koa/cors需要传递以下参数。

  /**
 * CORS middleware
 *
 * @param {Object} [options]
 *  - {String|Function(ctx)} origin `Access-Control-Allow-Origin`, default is request Origin header
 *  - {String|Array} allowMethods `Access-Control-Allow-Methods`, default is 'GET,HEAD,PUT,POST,DELETE,PATCH'
 *  - {String|Array} exposeHeaders `Access-Control-Expose-Headers`
 *  - {String|Array} allowHeaders `Access-Control-Allow-Headers`
 *  - {String|Number} maxAge `Access-Control-Max-Age` in seconds
 *  - {Boolean|Function(ctx)} credentials `Access-Control-Allow-Credentials`, default is false.
 *  - {Boolean} keepHeadersOnError Add set headers to `err.header` if an error is thrown
 * @return {Function} cors middleware
 * @api public
 */

// Example
app.use(cors())

CORS如何设置多域名

Access-Control-Allow-Origin,它所允许的值只有两个

  • *:所有域名
  • xianyu.sky:特定域名

那么CORS如果需要指定多个域名怎么办?

如果使用 Access-Control-Allow-Origin:*,则所有的请求不能够携带cookie,因此这种方案被摈弃。

因此这个问题需要写代码来解决,根据请求头中的Origin来设置响应头。

请求头:Origin

并不是所有请求都会自动带上Origin,在浏览器中带 Origin 的逻辑如下

  • 如果存在跨域,则带上Origin,值为当前域名。
  • 如果不存在跨域,则不带Origin 逻辑理清楚后,关于服务器中对于 Access-Control-Allow-Origin 设置多域名的逻辑也很清晰了。
  • 如果请求头不带有Origin,证明未跨域,则不作任何处理
  • 如果请求头带有Origin,证明跨域,根据Origin设置相应的Access-Control-Allow-Origin:

使用伪代码实现如下:

  // 获取Origin请求头
  const requestOrigin = ctx.get('Origin');
  
  // 如果没有,则跳过
  if(!requestOrigin){
  	return await next();
  }
  
  // 设置响应头
  ctx.set('Access-Control-Allow-Origin',requestOrigin)

CORS与Vary:Origin

如何避免CDN为PC端缓存移动端页面

如果PC端和移动端是一套代码则不会出现这个问题,这个问题出现在PC端和移动端是两套代码,但是却共用了一个域名。

使用 nginx 配置如下,根据UA判断是否移动端,而走不同的逻辑(判断UA是否移动端容易出问题)

location / {
  // 默认 PC 端
  root /usr/local/website/web;
  
  # 判断UA,访问移动端
  if ($http_user_agent ~* "(Android|webOS|iPhone|iPad|BlcakBerry)")
  root /usr/local/websitemobile;
  }
  
  index index.html index.htm;

解决方案通常使用 Vary 响应头,来控制CDN对不同请求头的缓存。

此处可以使用 Vary: User-Agent,代表如果User-Agent不一样,则重新发起请求,而非从缓存中读取页面

 Vary: User-Agent

当然,User-Agent实在过多,此时缓存失效就会过多。

简答

使用Vary:User-Agent,根据UA进行缓存。

  Vary:User-Agent

最好不要出现这种情况,PC端和移动端如果是两代码,建议用两个域名,理由如下

  • nginx 判断是否移动端容易出错
  • 对缓存不友好

假设有两个域名可以访问 static.xianyu.sky 的跨域资源

  • foo.xianyu.sky,响应头中返回 Access-Control-Allow-Origin:foo.xianyu.sky
  • bar.xianyu.sky,响应头中返回 Access-Control-Allow-Origin:bar.xianyu.sky

如果static.xianyu.sky 资源被CDN缓存,bar.xianyu.sky 再次访问资源时,因缓存问题,因此此时返回的是 Access-Control-Allow-Origin:foo.xianyu.sky,此时会有跨域问题。

此时,Vary:Origin就上场了,代表为不同的Origin 缓存不同的资源,这在各个服务器端CORS中间也能体现出来。

如下是koa关于CORS的处理函数

  return async function cors(ctx, next) {
  // If the Origin header is not present terminate this set of steps.
  // The request is outside the scope of this specification.}
  const requestOrigin = ctx.get('Origin');
  
  // Always set Vary header
  // https: //github.com/rs/cors/issues/10
  ctx.vary('Origin');

如下是Go语言关于CORS的处理函数

  func (c*Cors) handleActualRequest(w http.ResponseWriter, r *http.Request) {
  headers := w.Header()
  origin :=r.Header.Get("Origin")
  
  // Always set Vary,see https://github.com/rs/cors/issues/10
  headers.Add("Vary","Origin")
  }

进一步改进相关代码:

  // 获取Origin请求头
  const requestOrigin = ctx.get('Origin');
  
  // 不管有没有跨域都要设置 Vary: Origin
  ctx.set('Vary', 'Origin')
  
  // 如果没有设置,说明没有跨域,跳过
  if (!requestOrigin)
   {
  return await next();
  }
  // 设置响应头
  ctx.set('Access-Control-Allow-Origin',requestOrigin)

HSTS与CORS

HSTS(HTTP Strict Transport Security)为了避免HTTP跳转到HTTPS时遭受潜在的中间人攻击,由浏览器本身控制到HTTPS的跳转。如同CORS一样,它也是有一个服务器的响应头来控制。

  Strict-Transport-Security:max-age=5184000

此时浏览器访问该域名时,会使用307 Internal Redirect,无需服务器干涉,自动跳转到HTTPS请求。 如果前端访问HTTP跨域请求,此时浏览器通过HSTS跳转到HTTPS,但浏览器不会给出相应的CORS响应头部,就会发生跨域问题。

  GET / HTTP/1.1
  Host: xianyu.sky
  Origin: http://xianyu.sky
  User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36 Edg/85.0.564.44
  
  
Access to XMLHttpRequest at 'xxx' from origin 'xxx' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

服务器异常处理与跨域异常

当与其他中间件一起工作时,也有可能出现问题,由于不正确的执行顺序也可能导致跨域失败。

假设有一个参数校验中间件,置于CORS中间件上方,由于校验失败,并未穿过CORS中间件,在前端会报错跨域失败,真正的参数校验问题掩盖其中。

  const Koa = require('koa')
  const app = new Koa()
  const cors = require('@koa/cors')
  
  // 异常处理中间件
  app.use(async(ctx, next) => {
  	try {
  await next()} catch (e) {
  ctx.body = 'hello,error'}
  })
  
  // 某一个特定时刻肯定会报错的中间件
  app.use(async(ctx, next) => {
  throw new Error('hello,world')})
  
  // 某一个特定时刻肯定会报错的中间件
  app.use(async (ctx,next) => {
  throw new Error('hello,world')})
  
  // CORS中间件
  app.use(cors())
  app.listen(3000)

总结

本篇文章介绍了跨域问题及其相应的CORS解决方案,并列出若干细节问题。

  • CORS通过服务器端设置若干响应头来正常工作
  • Access-Control-Allow-Origin:* 无法携带Cookie,因此以此为多域名跨域设置有缺陷
  • 服务器端通过响应头Origin来判断是否为跨域请求,并以此设置多域名跨域,但要加上Vary:Origin
  • 在编码过程中要注意HSTS配置及服务器的中间件顺序带来的潜在风险