最近对思源笔记和 Obsidian 进行了一些了解,二者都是很好的离线个人知识库软件,并且二者都支持插件系统。在研究图床插件的过程中涉及到了 CORS 问题。
思源笔记和 Obsidian 都是基于 Electron 框架构建的软件,可以认为其中内置了 Chrominum 浏览器和 Node.js 环境。
- Node.js 是服务器端,不存在 CORS 问题。
- 浏览器环境存在 CORS 问题。
同样是 JavaScript 编程语言,Node.js 环境下进行 HTTP 请求和浏览器环境下进行 HTTP 请求是不一样的。
- Node.js 环境下有内置的
http
和https
包。 - 浏览器环境下有
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)。
简单请求
简单请求是指满足以下条件的(一般只考虑前面两个条件即可):
- 使用 GET、POST、HEAD 其中一种请求方法。
- HTTP的头信息不超出以下几种字段:
- Accept
- Accept-Language
- Content-Language
- Last-Event-ID
- Content-Type:只限于三个值
application/x-www-form-urlencoded
、multipart/form-data
、text/plain
- 请求中的任意 XMLHttpRequestUpload 对象均没有注册任何事件监听器;
- XMLHttpRequestUpload 对象可以使用 XMLHttpRequest.upload 属性访问。请求中没有使用 ReadableStream 对象。
这是为了兼容表单(form),因为历史上表单一直可以发出跨域请求。AJAX 的跨域设计就是,只要表单可以发,AJAX 就可以直接发。
对于简单请求,浏览器直接发起 CORS 请求,具体来说就是服务器端会根据请求头信息中的 Origin 字段(包括了协议 + 域名 + 端口),来决定是否同意这次请求。
|
|
如果 Origin 指定的源在许可范围内,服务器返回的响应,会多出几个头信息字段:
|
|
全部字段参考 CORS。
如果服务器没有返回相应的头部信息或 Origin 指定的源不在许可范围内,浏览器通过 onerror
抛出错误。此时不能通过 HTTP 状态码来识别请求是否成功。
非简单请求
非简单请求时指那些对服务器有特殊要求的请求,其实简单请求之外的都是非简单请求了。比如请求方法是 PUT 或 DELETE、Content-Type 的类型是 application/json
。
非简单请求的 CORS 请求,会在正式通信之前,使用 OPTIONS 方法发起一个预检(preflight)请求到服务器,浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些 HTTP 动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的 XMLHttpRequest / Fetch 请求,否则就报错。
下面是一个预检请求的头部:
|
|
一旦服务器通过了“预检”请求,以后每次浏览器正常的 CORS 请求,就都跟简单请求一样了。
Cookie
CORS 请求一般默认不发送 Cookie,如果服务器端允许 Cookie (即 Access-Control-Allow-Credentials: true
) 则可以通过设置 withCredentials=true
来要求浏览器发送 Cookie。
|
|
注意:服务器端如果要想允许 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 为例:
- 进入 Cloudflare R2 相应的 Bucket 页面。
- 进入 Settings 标签页并找到 CORS Policy。
- 点击 “Edit CORS policy” 进行编辑。以下是允许所有 Origin 访问的例子。更多详情参考官方文档。
1 2 3 4 5 6 7
[ { "AllowedOrigins": ["*"], "AllowedMethods": ["GET", "PUT", "POST", "HEAD", "DELETE"], "AllowedHeaders": ["*"] } ]
- 保存后可以通过 curl 命令进行测试。
没有报错且响应头部包含有 CORS 字段即表示生效。
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>/