JavaScript CORS 跨域请求

介绍 JavaScript 用 Fetch API 或 XMLHttpRequest API 进行跨域请求。并以思源笔记插件、Obsidian 插件、以及 S3 CORS 配置举例说明。

最近对思源笔记和 Obsidian 进行了一些了解,二者都是很好的离线个人知识库软件,并且二者都支持插件系统。在研究图床插件的过程中涉及到了 CORS 问题。

思源笔记和 Obsidian 都是基于 Electron 框架构建的软件,可以认为其中内置了 Chrominum 浏览器和 Node.js 环境。

  1. Node.js 是服务器端,不存在 CORS 问题。
  2. 浏览器环境存在 CORS 问题。

同样是 JavaScript 编程语言,Node.js 环境下进行 HTTP 请求和浏览器环境下进行 HTTP 请求是不一样的。

  1. Node.js 环境下有内置的 httphttps 包。
  2. 浏览器环境下有 XMLHttpRequest API 和 Fetch API。

当然上述只是最底层的API,在应用层面有各种第三方库进行封装,提供更加易用的接口。在浏览器端,传统的典型是 jQuery,新锐的典型是 Axios。

CORS 基础

说到 CORS 前,需要了解“同源”概念。同源协议域名端口三者完全相同。浏览器使用同源政策,目的是为了保证用户信息的安全,防止恶意的网站窃取数据,不同源的访问会受到限制(主要是 Cookie / Local Storage 访问、iframe DOM 访问、发起 HTTP 请求)。

对于 HTML 标签的外部链接如 <img><audio><video><script>,没有跨域问题。不过对于这样的外部链接请求不会带上 Cookie。
对于 JavaScript 发起 HTTP 请求,三要素有任何之一不匹配即是跨域,浏览器即会出于安全考虑进行限制,这时就需要使用 CORS (Cross-origin resource sharing)。CORS 主要由服务器端实现,对用户透明。
浏览器将 CORS 请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。

简单请求

简单请求是指满足以下条件的(一般只考虑前面两个条件即可):

  1. 使用 GET、POST、HEAD 其中一种请求方法。
  2. HTTP的头信息不超出以下几种字段:
    • Accept
    • Accept-Language
    • Content-Language
    • Last-Event-ID
    • Content-Type:只限于三个值 application/x-www-form-urlencodedmultipart/form-datatext/plain
  3. 请求中的任意 XMLHttpRequestUpload 对象均没有注册任何事件监听器;
  4. XMLHttpRequestUpload 对象可以使用 XMLHttpRequest.upload 属性访问。请求中没有使用 ReadableStream 对象。

这是为了兼容表单(form),因为历史上表单一直可以发出跨域请求。AJAX 的跨域设计就是,只要表单可以发,AJAX 就可以直接发。

对于简单请求,浏览器直接发起 CORS 请求,具体来说就是服务器端会根据请求头信息中的 Origin 字段(包括了协议 + 域名 + 端口),来决定是否同意这次请求。

1
2
3
4
5
6
GET /the-target-endpoint HTTP/1.1
Origin: http://www.bob.com
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

如果 Origin 指定的源在许可范围内,服务器返回的响应,会多出几个头信息字段:

1
2
3
Access-Control-Allow-Origin: http://www.bob.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: X-Custom-Header

全部字段参考 CORS

如果服务器没有返回相应的头部信息或 Origin 指定的源不在许可范围内,浏览器通过 onerror 抛出错误。此时不能通过 HTTP 状态码来识别请求是否成功。

非简单请求

非简单请求时指那些对服务器有特殊要求的请求,其实简单请求之外的都是非简单请求了。比如请求方法是 PUT 或 DELETE、Content-Type 的类型是 application/json
非简单请求的 CORS 请求,会在正式通信之前,使用 OPTIONS 方法发起一个预检(preflight)请求到服务器,浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些 HTTP 动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的 XMLHttpRequest / Fetch 请求,否则就报错。

下面是一个预检请求的头部:

1
2
3
4
5
6
7
8
OPTIONS /the-target-endpoint HTTP/1.1
Origin: http://www.bob.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

一旦服务器通过了“预检”请求,以后每次浏览器正常的 CORS 请求,就都跟简单请求一样了。

CORS 请求一般默认不发送 Cookie,如果服务器端允许 Cookie (即 Access-Control-Allow-Credentials: true) 则可以通过设置 withCredentials=true 来要求浏览器发送 Cookie。

1
2
3
4
5
6
7
8
const xhr = new XMLHttpRequest();
xhr.withCredentials = true;
xhr.send();

// OR
fetch(url, {
    credentials: 'include'
}).then(...);

注意:服务器端如果要想允许 Cookie,Access-Control-Allow-Origin 就不能是 *,必须指定明确的、与请求网页一致的域名。否则即使指定了 withCredentials=true ,浏览器也不会发送 Cookie。

思源笔记插件处理 CORS

思源笔记的插件如果有外部资源请求,需要考虑 CORS。桌面版本没有 CORS 问题,移动端或浏览器版有 CORS 问题。
如果外部资源允许当前 Origin (比如 http://127.0.0.1:6806),那不需要特殊处理,正常使用 Fetch API 或 XMLHttpRequest API 即可。
如果外部资源不允许当前 Origin,那么需要使用一个代理来中转请求。代理本身是服务端环境,访问目标资源时没有跨域问题,代理本身则允许当前 Origin。
思源内部实现了一个 Proxy:/api/network/forwardProxy。详情参考文档

思源图床插件 PicGo (siyuan-plugin-picgo),就支持通过内置的代理处理 CORS 以支持特定的图床服务。

Obsidian 插件处理 CORS

Obsidian 插件和思源插件面临同样的情况。Obsidian 提供了 requestUrl 供插件来处理 CORS。

Obsidian 插件 S3 Image Uploader (s3-image-uploader) 和 Imgur (obsidian-imgur-plugin),就是通过内置的 requestUrl 处理 CORS。

S3 服务处理 CORS

Amazon S3 和 有些 S3 兼容服务是允许在浏览器中直接访问存储桶的,所以支持在存储桶层面设置 CORS 策略。具体可以参考不同服务的官方文档。

这里以 Cloudflare R2 为例:

  1. 进入 Cloudflare R2 相应的 Bucket 页面。
  2. 进入 Settings 标签页并找到 CORS Policy
  3. 点击 “Edit CORS policy” 进行编辑。以下是允许所有 Origin 访问的例子。更多详情参考官方文档
    1
    2
    3
    4
    5
    6
    7
    
    [
      {
        "AllowedOrigins": ["*"],
        "AllowedMethods": ["GET", "PUT", "POST", "HEAD", "DELETE"],
        "AllowedHeaders": ["*"]
      }
    ]
    
  4. 保存后可以通过 curl 命令进行测试。
    1
    2
    3
    4
    5
    
    curl -H “Origin: http://127.0.0.1:6806” \
    -H “Access-Control-Request-Method: PUT” \
    -H “Access-Control-Request-Headers: X-Requested-With” \
    -X OPTIONS --verbose \
    https://<prefix>.r2.cloudflarestorage.com/<bucket>/
    
    没有报错且响应头部包含有 CORS 字段即表示生效。