Nginx 跨域资源共享 CORS

第一次为 Web App 提供 REST API。

这学期期末 Project Codo 是一个多人协作支持订阅的备忘录,在 Web App 上打算是使用 Client-Side-Rendering 的一个 Website,ajax 配合 RESTful API 完成内容展示,由于 API 和 Web App 的域名不一样,遇到了 XMLHttpRequest 的 CORS 问题。

实际上要解决这些问题,只需要在 Google 搜索一下 CORS 基本就会有很万能的解决方案,例如搜索结果第三的这一篇。但是作为一个 Web 开发者,怎么能不去深究一下呢?

CORS 问题来源于浏览器出于安全考虑,会限制脚本中发起的跨站请求。比如,使用 XMLHttpRequest 对象发起 HTTP 请求就必须遵守同源策略。具体而言,Web 应用程序能且只能使用 XMLHttpRequest 对象向其加载的源域名发起 HTTP 请求,而不能向任何其它域名发起请求。但是这样的限制会在 Web 开发中造成不便,以 Codo 为例,我们用 AngularJS 开发的前端实际上是纯静态的,为了访问速度可以丢到 CDN 上,而后端为了初期为了避免数据库同步问题,只有单一实例,换言之静态数据和动态数据不在同一台服务器上,如果限制两者域名必须相同,就不能实现我们动静分离的设想了。

W3C 为了解决上面提到的问题,提出了 CORS。

在 CORS 中,W3C 把请求分为了简单请求和非简单请求,同时满足下面两个条件则为简单请求。为何要区分简单请求,等下会解答。

  • 只使用 GET, HEAD 或者 POST 请求方法。如果使用 POST 向服务器端传送数据,则数据类型 (Content-Type) 只能是 application/x-www-form-urlencoded, multipart/form-datatext/plain 中的一种。
  • 不会使用自定义请求头(类似于 X-Modified 这种)。

在简单请求中,只需要服务端返回的 HTTP Response 包含 Access-Control-Allow-Origin: http://foo.example,浏览器即会接受这次请求。

下面再说回非简单请求,为什么会有非简单请求的这一种划分?因为在上面的描述可以知道浏览器是根据服务端返回的 HTTP Response 来判断是否接受这一次请求,此时 Request 早已发出去,如果是 PUT 和 DELETE 这些请求,很可能服务器已对请求进行处理了,而浏览器拒绝了,会造成不一致的情况。为了解决这些非简单请求造成的影响,浏览器会使用 HTTP 中 OPTIONS 的方法,询问一下服务器是否允许某个方法。此外,OPTION 请求会附带下面这两个 header,

Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER

此时的 Response 除了上面提到的 Access-Control-Allow-Origin: http://foo.example 之外,还需要附带 Access-Control-Allow-Methods: POST, GET, OPTIONSAccess-Control-Allow-Headers: X-PINGOTHER。另外 Access-Control-Max-Age: 1728000 也是推荐的,它告诉浏览器这个 OPTIONS 请求可以保留多久,而不需要每次非简单请求前发送 OPTIONS。

根据上面的分析,我们可以得出一份比较通用的 Nginx 配置,这个也是 Codo API Server 目前在使用的。

location / {
    if ($request_method = 'OPTIONS') {
        add_header 'Access-Control-Allow-Origin' '*';
        add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, DELETE';
        add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
        add_header 'Access-Control-Max-Age' 1728000;
        add_header 'Content-Type' 'text/plain charset=UTF-8';
        add_header 'Content-Length' 0;
        return 204;
    }
    add_header 'Access-Control-Allow-Origin' '*';
    add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, DELETE';
    add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
}