跳到主要内容

CORS 跨域方案

2024年03月20日
柏拉文
越努力,越幸运

一、认识


跨源资源共享 Cross-origin Resource SharingCORS,或通俗地译为跨域资源共享)用于在跨域请求中安全地共享资源。它通过在服务器端和客户端之间使用一系列 HTTP 头,明确哪些跨域请求是被允许的,从而既保护了用户数据的安全,又满足了现代 Web 应用跨域数据交互的需求。

跨源资源共享 Cross-origin Resource Sharing (CORS) 原理: 当浏览器发起跨域请求时, 会在 HTTP 请求头中自动添加 Origin 头, 标明发起请求的源(协议、域名、端口)。服务器在响应时, 需要返回相应的 CORS 头, 如 Access-Control-Allow-Origin 来指定允许哪些源可以访问资源。如果响应头中没有正确的 CORS 信息, 浏览器会拒绝前端代码读取该响应内容。浏览器发起跨域请求分为简单请求和复杂请求。

一、简单请求:

  1. CORS 简单请求必须满足以下条件: 1. HTTP 请求方法 必须为 GETHEADPOST; 2. 请求头必须在 AcceptAccept-LanguageContent-LanguageContent-Type 范围之内, 不包含自定义的请求头。3. 请求 Content-Type 类型 必须是 application/x-www-form-urlencodedmultipart/form-datatext/plain

  2. CORS 简单请求流程: 1. 浏览器发起请求并带上 Origin 头; 2. 服务器检查请求的 Origin 是否在允许范围内; 3. 如果允许, 服务器在响应中设置 Access-Control-Allow-Origin(可以是具体的域名或 *,但若需要携带凭证,则不能为 *),以及其他相关头(如 Access-Control-Allow-Credentials); 4. 浏览器接收到响应, 检查 CORS 头部信息后决定是否将响应交给前端 JavaScript

二、预检请求(复杂请求):

  1. CORS 预检请求满足的条件: 简单请求之外的都为 预检请求(复杂请求)。(例如使用 PUTDELETEPATCH 方法,或自定义请求头,或使用非标准 Content-Type, 比如: Content-Type: application/json)。

  2. CORS 预检请求流程: 1. 浏览器会在正式请求之前自动发起一个 HTTP OPTIONS 请求,即预检请求, 请求中包含 Origin 头以及 Access-Control-Request-Method(表明实际请求将使用的方法)和可能的 Access-Control-Request-Headers(表明实际请求中将携带的自定义头); 2. 服务器必须返回允许的域名 Access-Control-Allow-Origin(可以是具体的域名或 *,但若需要携带凭证,则不能为 *), 允许的 HTTP 方法列表(通过 Access-Control-Allow-Methods), 允许的请求头(通过 Access-Control-Allow-Headers), 以及预检请求的缓存时间(Access-Control-Max-Age); 3. 浏览器根据预检响应判断, 如果预检请求通过,浏览器才会发送正式的跨域请求;否则,浏览器会终止请求并报告错误。

三、Cors 请求头、响应头

  • 简单请求: 浏览器端需要发送 Origin 请求头, 服务器需要返回 Access-Control-Allow-Origin 响应头

  • 预检请求: 浏览器端需要发送 OriginAccess-Control-Request-MethodAccess-Control-Request-Headers, 服务器需要返回 Access-Control-Allow-OriginAccess-Control-Allow-MethodAccess-Control-Allow-Headers

二、实现


2.1 CORS 中间件

// cors.js
module.exports = function cors(options = {}) {
// 默认配置
const defaultOptions = {
origin: '*', // 默认允许所有域名
allowMethods: 'GET,HEAD,PUT,POST,DELETE,PATCH,OPTIONS',
allowHeaders: 'Content-Type, Authorization, Accept, X-Requested-With',
maxAge: 5, // 预检请求的缓存时间,单位秒
credentials: false // 是否允许携带 Cookie
};

// 合并用户配置与默认配置
const opts = Object.assign({}, defaultOptions, options);

return async function corsMiddleware(ctx, next) {
// 获取请求中的 Origin
const requestOrigin = ctx.get('Origin');
if (requestOrigin) {
// 如果配置的 origin 为 '*',则直接允许所有域名,否则使用指定的域名
ctx.set('Access-Control-Allow-Origin', opts.origin === '*' ? '*' : opts.origin);

// 如果允许携带 Cookie,则必须指定具体域名,不能为 '*'
if (opts.credentials) {
ctx.set('Access-Control-Allow-Credentials', 'true');
}
// 增加 Vary 头,告诉缓存服务器根据 Origin 缓存不同响应
ctx.append('Vary', 'Origin');
}

// 设置允许的方法、请求头、以及预检请求缓存时间
ctx.set('Access-Control-Allow-Methods', opts.allowMethods);
ctx.set('Access-Control-Allow-Headers', opts.allowHeaders);
if (opts.maxAge) {
ctx.set('Access-Control-Max-Age', opts.maxAge.toString());
}

// 对于预检请求(OPTIONS),直接响应 204,不继续后续中间件
if (ctx.method === 'OPTIONS') {
ctx.status = 204;
return;
}

// 执行后续中间件
await next();
};
};

2.2 Node Koa Server

const Koa = require('koa');
const cors = require('./cors'); // 引入我们实现的 cors 插件

const app = new Koa();

// 使用自定义的 cors 中间件(可传入配置参数)
app.use(cors({
origin: 'https://example.com',
credentials: true,
maxAge: 600
}));

app.use(async (ctx) => {
ctx.body = 'Hello, CORS!';
});

app.listen(3000, () => {
console.log('Server is running on http://localhost:3000');
});

五、问题

5.1 HTTP 跨域时为何要发送 options 请求?

CORS 预检请求满足的条件: 简单请求之外的都为 预检请求(复杂请求)。(例如使用 PUTDELETEPATCH 方法,或自定义请求头,或使用非标准 Content-Type, 比如: Content-Type: application/json)。

CORS 预检请求流程: 1. 浏览器会在正式请求之前自动发起一个 HTTP OPTIONS 请求,即预检请求, 请求中包含 Origin 头以及 Access-Control-Request-Method(表明实际请求将使用的方法)和可能的 Access-Control-Request-Headers(表明实际请求中将携带的自定义头); 2. 服务器必须返回允许的域名 Access-Control-Allow-Origin(可以是具体的域名或 *,但若需要携带凭证,则不能为 *), 允许的 HTTP 方法列表(通过 Access-Control-Allow-Methods), 允许的请求头(通过 Access-Control-Allow-Headers), 以及预检请求的缓存时间(Access-Control-Max-Age); 3. 浏览器根据预检响应判断, 如果预检请求通过,浏览器才会发送正式的跨域请求;否则,浏览器会终止请求并报告错误。

参考资料


说一下 CORS 的简单请求和复杂请求的区别