用代码来浅说CORS那些事儿

引言

CORS 的全称是 Cross-Origin Resource Sharing, 翻译过来就是跨域资源共享。简单来说就是一个资源发起一个跨域 (不同域名或者不同端口) 的HTTP 请求来获取资源, 具体的定义详见 w3c 的标准.

出于安全考虑,浏览器会限制脚本中发起的跨域请求。比如,使用 XMLHttpRequest 和 Fetch 发起的 HTTP 请求必须遵循同源策略。但是为提升 Web 应用的可用性,浏览器必须支持跨域请求。那么如何对跨域的请求做访问控制呢,就是通过 CORS 机制来控制的。 CORS 需要客户端和服务器同时支持。目前,所有浏览器都支持该机制。而服务端的支持则有开发来进行控制实现。

浅说

我们都知道客户端向服务端发送的 Request 以及服务端向客户端返回的 Response 都携带这 HTTP头,HTTP 消息头用来准确描述正在获取的资源、服务器或者客户端的行为. 而 CORS 也正是通过 HTTP Request 和 Response 的消息头来完成控制的。

首先了解一下 HTTP 消息头的下面这个几个域:

而常见的跨域请求会有下面几种场景:

比如 www.a.com 请求 www.b.com 的资源。使用下面的 HTTP 方法: GET, HEAD, 或者 POST. 而且请求的 Content-Type 属于下面几种之一: application/x-www-form-urlencoded, multipart/form-data, text/plain.

同样是 wwww.a.com 请求 www.b.com 的资源,但是如果使用了这些方法: PUT, DELETE, CONNECT, OPTIONS, TRACE, PATCH 或者请求的 Content-Type 不属于application/x-www-form-urlencoded, multipart/form-data, text/plain 之一。预检的请求要求必须首先使用 OPTIONS 方法发起一个预检请求到服务器,以获知服务器是否允许该实际请求。"预检请求"的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响。

对于跨域 XMLHttpRequest 或 Fetch 请求,浏览器不会发送身份凭证信息,如果要在请求中携带身份信息比如 Cookie, 则需要设置 XMLHttpRequest 的某个特殊标志位 (withCredentials), 如果服务器端的响应中未携带 Access-Control-Allow-Credentials: true ,浏览器将不会把响应内容返回给请求的发送者。

代码呢?

让我们基于 Koa (服务端) 和 fetch (客户端) 用代码来描述上面的场景吧, 你也可以在我最近的开源项目(YoYo,一个基于 Koa 和 React 的评论服务) 看到在生产环境如果实现 CORS 的.

import Koa from 'koa'
import cors from 'koa-cors'

const app = new Koa()
app.use(cors({ origin: '*' }))

在客户端直接调用 fetch 即可访问服务端资源

fetch(url, { method: 'GET', ... })
import Koa from 'koa'
import cors from 'koa-cors'

const app = new Koa()
app.use(cors({
  origin: 'YourOrigin',
  credentials: true,
}))

这样写导致的结果是,只有 'YourOrigin' 这个域的请求能够访问到服务端域的资源,有没有更灵活的方法呢,有的,可以这样:

import Koa from 'koa'
import cors from 'koa-cors'

const app = new Koa()
const options = {
  origin: (ctx) => {
    const origin = ctx.headers.origin
    const whiteList = ['https://a.com', 'https://b.com']
    //
    // if request with credentials, origin cannot be '*',
    // origin should be exactly the request origin
    //
    if (whiteList.indexOf(origin) > -1) {
      return origin
    }
    return '*'
  },
  credentials: true,
}
app.use(cors(options))

这样就可以达到,白名单里面的域名的请求可以携带 credential 而正确接受到资源,而其它域名的请求则属于变成简单跨域请求。

而客户端则可以这样发起请求:

fetch(url, {
  method: 'POST',
  credentials: 'include',
  headers: {
    'Content-Type': 'application/json',
  },
})

最后

如果你要自己做一些实验的话,搭建一个简单的 HTTP Server 然后打开浏览器就可以了,如果你在用 Koa 的话,有两个个小窍门可以参考:

ctx.response.once('finish', () => { // your codes here })
ctx.response.once('close', () => { // your codes here })

/etc/hosts

127.0.0.1 abc.com

拓展阅读

HTTP Access Control