CORS 跨域方案
一、认识
跨源资源共享 Cross-origin Resource Sharing
(CORS
,或通俗地译为跨域资源共享)用于在跨域请求中安全地共享资源。它通过在服务器端和客户端之间使用一系列 HTTP
头,明确哪些跨域请求是被允许的,从而既保护了用户数据的安全,又满足了现代 Web
应用跨域数据交互的需求。
跨源资源共享 Cross-origin Resource Sharing
(CORS
) 原理: 当浏览器发起跨域请求时, 会在 HTTP
请求头中自动添加 Origin
头, 标明发起请求的源(协议、域名、端口)。服务器在响应时, 需要返回相应的 CORS
头, 如 Access-Control-Allow-Origin
来指定允许哪些源可以访问资源。如果响应头中没有正确的 CORS
信息, 浏览器会拒绝前端代码读取该响应内容。浏览器发起跨域请求分为简单请求和复杂请求。
一、简单请求:
-
CORS
简单请求必须满足以下条件: 1.HTTP 请求方法
必须为GET
、HEAD
或POST
; 2. 请求头必须在Accept
、Accept-Language
、Content-Language
、Content-Type
范围之内, 不包含自定义的请求头。3. 请求Content-Type
类型 必须是application/x-www-form-urlencoded
、multipart/form-data
或text/plain
。 -
CORS
简单请求流程: 1. 浏览器发起请求并带上Origin
头; 2. 服务器检查请求的Origin
是否在允许范围内; 3. 如果允许, 服务器在响应中设置Access-Control-Allow-Origin
(可以是具体的域名或*
,但若需要携带凭证,则不能为*
),以及其他相关头(如Access-Control-Allow-Credentials
); 4. 浏览器接收到响应, 检查CORS
头部信息后决定是否将响应交给前端JavaScript
。
二、预检请求(复杂请求):
-
CORS
预检请求满足的条件: 简单请求之外的都为 预检请求(复杂请求)。(例如使用PUT
、DELETE
、PATCH
方法,或自定义请求头,或使用非标准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
请求头、响应头
-
简单请求: 浏览器端需要发送
Origin
请求头, 服务器需要返回Access-Control-Allow-Origin
响应头 -
预检请求: 浏览器端需要发送
Origin
、Access-Control-Request-Method
、Access-Control-Request-Headers
, 服务器需要返回Access-Control-Allow-Origin
、Access-Control-Allow-Method
、Access-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
预检请求满足的条件: 简单请求之外的都为 预检请求(复杂请求)。(例如使用 PUT
、DELETE
、PATCH
方法,或自定义请求头,或使用非标准 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. 浏览器根据预检响应判断, 如果预检请求通过,浏览器才会发送正式的跨域请求;否则,浏览器会终止请求并报告错误。