cors 学习笔记
同源策略
- 作用:用来控制不同源之间的交互。
- 定义:如果两个 URL 的协议、端口和域名都相同的话,则这两个 URL 就是同源的。
不同源之间的交互:
- 跨域写操作:一般是被允许的,如链接、重定向、表单提交。
- 跨域资源嵌入:一般是被允许的,如img、script标签(JSONP实现跨域的前提条件)。
- 跨域读操作:一般是不被允许的,读操作指的是从服务器获取资源(有response),即所有http接口请求都不被允许。
浏览器自己是可以发起跨域请求的(比如a标签、img标签、form表单等),但是Javascript是不能去跨域获取资源(如ajax)。
跨域问题的控制台报错截图:
跨域资源共享
跨域资源共享(CORS)是一种基于 HTTP 首部的机制。
- 该机制通过允许服务器标识除了它自己以外的其它 origin,这样浏览器可以访问加载这些资源。
- 使用
options方法发起预检请求来询问服务器是否允许要发送的真实请求。在预检请求中,浏览器发送的首部中标识有 HTTP 方法和真实请求中会用到的首部。
CORS 的几个使用场景:
- XMLHttpRequest 和 Fetch 发起的跨源 HTTP 请求
- css 中通过
@font-face使用跨源字体资源 - 使用
drawImage将 Images/Videos 画面绘制到 Canvas
某些请求不会触发 CORS 预检请求,称这样的请求为简单请求。关于简单请求的限制条件,可看MDN 介绍
相关响应报文首部
Access-Control-Allow-Origin:该响应的资源是否被允许与给定的 origin 共享
Access-Control-Allow-Origin: *
Access-Control-Allow-Origin: https://developer.mozilla.org
Access-Control-Allow-Methods:在对预检请求的应答中,明确了客户端所将访问的资源允许使用的方法。
Access-Control-Allow-Methods: POST,GET,OPTIONS
Access-Control-Expose-Headers:列出了哪些首部可以作为响应的一部分暴露给外部。默认情况下只有七种首部可以暴露给外部
Access-Control-Allow-Headers:在对预检请求的应答中,列出了可以在真实请求中出现的请求首部
Access-Control-Max-Age:表示预检请求的应答结果中的信息可以被缓存多久
Access-Control-Allow-Credentials:是否允许客户端携带验证信息,如 cookie。默认机制下,cors 不会携带 cookie 的。
- 服务器端的响应报文首部需要带
Access-Control-Allow-Credentials: true - 浏览器端发起 ajax 需要指定
withCredentials为true - 响应报文首部中的
Access-Control-Allow-Origin不能为*
相关请求报文首部
Origin:浏览器会将Origin请求首部添加到:所有的跨域请求、除 GET、HEAD 请求外的同源请求
preflight request 预检请求:请求方法为 options,当有需要的时候,浏览器会自动发出一个预检请求,不需要前端开发者自己去发。
preflight request 一般包括的请求首部有:
OriginAccess-Control-Reuqest-MethodAccess-Control-Request-Headers
@koa/cors
使用方式:
const Koa = require("koa");
const cors = require("@koa/cors");
const app = new Koa();
app.use(cors());
主要源码的阅读:
// 省略一些配置项格式化、合并代码
return async function cors(ctx, next) {
// 如果Origin请求首部不存在,则结束cors的配置,因为请求不符合规范
const requestOrigin = ctx.get("Origin");
// Always set Vary header
// https://github.com/rs/cors/issues/10
ctx.vary("Origin");
// 跳出
if (!requestOrigin) return await next();
// origin的处理配置
let origin;
if (typeof options.origin === "function") {
origin = options.origin(ctx);
if (origin instanceof Promise) origin = await origin;
if (!origin) return await next();
} else {
origin = options.origin || requestOrigin;
}
// credentials的处理配置
let credentials;
if (typeof options.credentials === "function") {
credentials = options.credentials(ctx);
if (credentials instanceof Promise) credentials = await credentials;
} else {
credentials = !!options.credentials;
}
const headersSet = {};
function set(key, value) {
ctx.set(key, value);
headersSet[key] = value;
}
// 预检请求使用的HTTP方法是options
if (ctx.method !== "OPTIONS") {
// Simple Cross-Origin Request, Actual Request, and Redirects
set("Access-Control-Allow-Origin", origin);
if (credentials === true) {
// 允许客户端携带验证信息
set("Access-Control-Allow-Credentials", "true");
}
if (options.exposeHeaders) {
// 预检请求才配置Access-Control-Expose-Headers
set("Access-Control-Expose-Headers", options.exposeHeaders);
}
if (!options.keepHeadersOnError) {
return await next();
}
try {
return await next();
} catch (err) {
const errHeadersSet = err.headers || {};
const varyWithOrigin = vary.append(
errHeadersSet.vary || errHeadersSet.Vary || "",
"Origin"
);
delete errHeadersSet.Vary;
err.headers = {
...errHeadersSet,
...headersSet,
...{ vary: varyWithOrigin },
};
throw err;
}
} else {
// Preflight Request 预检请求
/**
*如果解析不出Access-Control-Request-Method请求首部,
*则不是预检请求,直接终止跳出
*/
if (!ctx.get("Access-Control-Request-Method")) {
// this not preflight request, ignore it
return await next();
}
ctx.set("Access-Control-Allow-Origin", origin);
if (credentials === true) {
// 允许客户端携带验证信息
ctx.set("Access-Control-Allow-Credentials", "true");
}
if (options.maxAge) {
// 在指定的时间内,不再需要发送预检请求
ctx.set("Access-Control-Max-Age", options.maxAge);
}
if (options.allowMethods) {
ctx.set("Access-Control-Allow-Methods", options.allowMethods);
}
let allowHeaders = options.allowHeaders;
if (!allowHeaders) {
allowHeaders = ctx.get("Access-Control-Request-Headers");
}
if (allowHeaders) {
ctx.set("Access-Control-Allow-Headers", allowHeaders);
}
// 204 No Content 响应报文中没有实体的主体部分
ctx.status = 204;
}
};
expressjs/cors
使用方式:
const express = require("express");
const cors = require("cors");
const app = express();
app.use(cors());
主要源码的阅读:
function cors(options, req, res, next) {
var headers = [],
method = req.method && req.method.toUpperCase && req.method.toUpperCase();
if (method === "OPTIONS") {
// preflight 预检请求
headers.push(configureOrigin(options, req)); // 根据不同情况设置Access-Control-Allow-Origin、Vary: Origin
headers.push(configureCredentials(options, req)); // 配置Access-Control-Allow-Credentials
headers.push(configureMethods(options, req)); // 配置Access-Control-Allow-Methods
headers.push(configureAllowedHeaders(options, req)); // 配置Access-Control-Allow-Headers
headers.push(configureMaxAge(options, req)); // 配置Access-Control-Max-Age
// 预检请求才配置Access-Control-Expose-Headers
headers.push(configureExposedHeaders(options, req)); // 配置Access-Control-Expose-Headers
applyHeaders(headers, res);
if (options.preflightContinue) {
next();
} else {
// Safari (and potentially other browsers) need content-length 0,
// for 204 or they just hang waiting for a body
res.statusCode = options.optionsSuccessStatus;
res.setHeader("Content-Length", "0");
res.end();
}
} else {
// actual response 真实请求
headers.push(configureOrigin(options, req));
headers.push(configureCredentials(options, req));
headers.push(configureExposedHeaders(options, req));
applyHeaders(headers, res);
next();
}
}
存在的疑问
对于Vary首部、Vary: Origin的用法和含义,我不理解,如果有知道的网友,可以在评论区写出来~