remix 的 cors-middleware 完全指南:从预检短路到动态 Origin 策略的 Fetch API 跨域方案
【免费下载链接】remixThe fully-stacked web framework项目地址: https://gitcode.com/GitHub_Trending/re/remix
导读
cors-middleware是 remix 仓库中面向 Fetch API 服务器的标准 CORS 中间件,负责为响应附加标准跨域头,并智能处理OPTIONS预检请求——既可短路预检,也可透传给应用自定义的OPTIONS处理器。读完本文,你将掌握它的全部配置项(Origin 匹配、凭据、暴露头、私有网络预检等),理解预检短路的底层实现原理,并能直接在基于fetch-router的 API 服务中落地可复制的跨域方案。
功能总览
根据 packages/cors-middleware/README.md,该中间件提供以下核心能力:
- 预检处理(Preflight Handling):自动处理
OPTIONS预检请求; - 灵活的 Origin 规则(Flexible Origin Rules):支持静态字符串、正则、数组与函数四种 Origin 策略;
- 凭据支持(Credential Support):支持携带凭据的请求,并做符合规范的 Origin 反射;
- 请求头控制(Header Controls):可配置允许头、暴露头、预检方法与缓存时长;
- 私有网络支持(Private Network Support):可选地放行私有网络预检请求。
安装方式(它是 remix monorepo 中的一个 workspace 包,完整安装 remix 即可使用):
npm i remix该包的清单文件见 packages/cors-middleware/package.json,发布名为@remix-run/cors-middleware,导出入口为src/index.ts,源码位于 packages/cors-middleware/src/lib/cors.ts。
快速上手
中间件与fetch-router的createRouter直接组合,在middleware数组中注册即可生效:
import { createRouter } from 'remix/router' import { cors } from 'remix/middleware/cors' let router = createRouter({ middleware: [ cors({ origin: ['https://app.example.com', 'https://admin.example.com'], credentials: true, exposedHeaders: ['X-Request-Id'], }), ], }) router.get('/api/projects', () => { return Response.json([{ id: 'p1', name: 'Remix' }], { headers: { 'X-Request-Id': 'req_123', }, }) })启动后,所有响应都会附带与请求匹配的Access-Control-*响应头,浏览器跨域调用即可正常读取数据与自定义响应头。
Origin 策略(Origin Policies)
origin配置项支持以下全部取值形式(对应源码中的CorsOrigin类型,见 cors.ts):
| 取值 | 含义 |
|---|---|
'*' | 允许所有来源 |
string | 单个精确 Origin,必须与请求Origin完全相等才放行 |
RegExp | 基于正则的模式匹配 |
Array<string \| RegExp> | 多个精确值与模式的混合匹配 |
true | 反射请求 Origin(把请求的Origin原样写回响应头) |
false | 完全禁用 CORS 响应头(返回null,等价于不匹配) |
(origin, context) => boolean \| string | 动态策略函数 |
限制来源(Restrict Origins)
用数组精确列举可信来源是 API 服务的常见做法:
let router = createRouter({ middleware: [ cors({ origin: ['https://app.example.com', 'https://admin.example.com'], credentials: true, }), ], })从源码resolveAllowedOrigin(cors.ts)可以看到匹配逻辑:字符串要求严格相等,正则通过RegExp.test判断,数组逐项遍历,命中即返回请求 Origin;全部未命中返回null,此时预检请求会被中间件以403短路拒绝(对应测试 cors.test.ts)。
动态 Origin 策略(Dynamic Origin Policies)
当允许策略依赖请求上下文(例如路径、请求头)时,使用函数形式:
let router = createRouter({ middleware: [ cors({ origin(origin, context) { if (context.url.pathname.startsWith('/public/')) { return '*' } return origin.endsWith('.trusted.example') }, }), ], })函数接收两个参数:请求的Origin字符串与RequestContext。从源码看,context提供了headers、url、method、request等字段(request-context.ts)。函数返回值经过normalizeResolvedOrigin(cors.ts)归一化:true反射请求 Origin、'*'通配、false/null/undefined表示拒绝、字符串作为精确值输出;返回 Promise 同样支持。测试用例 cors.test.ts 验证了origin.endsWith('.trusted.example')的动态匹配行为。
一个值得注意的细节:正则匹配时源码会基于pattern.source与pattern.flags重建一个新的RegExp实例(见matchesOriginPattern),因此即使配置了带g标志的正则,跨多次请求也能保持一致匹配结果,不会因 lastIndex 状态残留而失效(测试 cors.test.ts 专门覆盖了这一点)。
预检行为(Preflight Behavior)
默认短路:204
默认情况下,预检请求会被中间件短路,返回状态码204(无响应体):
let router = createRouter({ middleware: [ cors({ methods: ['GET', 'POST', 'PATCH'], allowedHeaders: ['Authorization', 'Content-Type'], maxAge: 600, }), ], })相关配置项:
methods:预检响应的Access-Control-Allow-Methods,默认值为['GET', 'HEAD', 'PUT', 'PATCH', 'POST', 'DELETE'](见 cors.ts)。注意配置会被统一转换为大写并去重(normalizeMethodList)。allowedHeaders:预检响应的Access-Control-Allow-Headers。未配置时默认反射请求携带的Access-Control-Request-Headers(见 cors.ts)。maxAge:Access-Control-Max-Age(秒),源码会执行Math.max(0, Math.floor(options.maxAge))归一化为非负整数后才写入响应头。
中间件判断预检请求的条件是context.method === 'OPTIONS'且请求头存在Access-Control-Request-Method(见isPreflightRequest,cors.ts)。预检响应会同时设置Vary: Access-Control-Request-Method,确保缓存正确区分不同方法的预检结果。
基于请求的 allowedHeaders 策略
当允许头列表需要随请求动态变化时,传入函数:
let router = createRouter({ middleware: [ cors({ allowedHeaders(request) { let requestedHeaders = request.headers.get('Access-Control-Request-Headers') if (requestedHeaders?.includes('x-admin-token')) { return ['Authorization', 'Content-Type', 'X-Admin-Token'] } return ['Authorization', 'Content-Type'] }, }), ], })函数接收Request与RequestContext两个参数,可同步或异步返回string[]。基于函数的allowedHeaders响应随Access-Control-Request-Headers变化,因此中间件会自动为这类响应附加Vary: Access-Control-Request-Headers,缓存不会复用不同请求头集合下的预检响应(对应源码varyOnRequestHeaders逻辑与测试 cors.test.ts)。若函数返回null/undefined,则回退为反射浏览器请求的头列表(测试见 cors.test.ts)。
透传与自定义状态码
- 设置
preflightContinue: true后,预检请求不再被短路,而是继续进入下游处理器(例如你自行注册的router.options(...)处理器),此时 CORS 响应头仍会附加到最终响应上。测试 cors.test.ts 验证了preflightContinue透传后仍保留Access-Control-Allow-Origin与Access-Control-Allow-Methods。 - 使用
preflightStatusCode可改变短路预检的响应状态码(默认204)。该配置同样作用于"请求无Origin头但为预检请求"的情况(见 cors.ts)。
私有网络预检(Private Network Preflights)
let router = createRouter({ middleware: [ cors({ allowPrivateNetwork: true, }), ], })启用allowPrivateNetwork后,当预检请求携带Access-Control-Request-Private-Network: true时,中间件会在响应中附加Access-Control-Allow-Private-Network: true(源码见 cors.ts),同时把Access-Control-Request-Private-Network加入Vary。该行为由测试 cors.test.ts 验证。这适用于本地开发联调、内网管理后台等需要从浏览器访问私有网络资源的场景。
暴露响应头(Expose Response Headers)
默认情况下,浏览器跨域环境下 JS 只能读取 CORS-safelisted 响应头,自定义响应头(如X-Request-Id、X-Trace-Id)必须通过exposedHeaders显式暴露:
let router = createRouter({ middleware: [ cors({ exposedHeaders: ['X-Request-Id', 'X-Trace-Id'], }), ], })从源码看(cors.ts),exposedHeaders只作用于实际请求(非预检),生成Access-Control-Expose-Headers响应头,配置头名会经过去空白与大小写去重(normalizeHeaderList)。测试 cors.test.ts 验证了输出格式X-Request-Id, X-Trace-Id。
源码实现要点与缓存安全
理解底层实现有助于在生产环境正确使用:
- 请求处理顺序(cors.ts):中间件先读取请求
Origin;无Origin头时直接放行(预检请求除外);解析允许 Origin 失败时,预检短路403,普通请求放行;匹配成功后构造 CORS 头,预检请求短路或继续,普通请求调用next()后把 CORS 头合并进响应。 - 凭据与通配的冲突处理:当
credentials: true与origin: '*'同时出现时,Access-Control-Allow-Origin不会被写成*(浏览器禁止凭据模式下的通配),而是反射请求 Origin,并附加Vary: Origin,保证缓存安全(见 cors.ts)。 - Vary 合并机制:中间件基于
@remix-run/headers/vary提供的Vary类(见 packages/headers/src/lib/vary.ts)维护去重、小写归一化的Vary集合;若下游响应本身已带Vary(如Accept-Encoding),withCorsHeaders会将其与 CORS 相关 Vary 合并输出(测试 cors.test.ts)。 - 响应构造:短路响应通过
new Response(null, { status, headers })生成;合并响应则基于原响应重建Response,保留status、statusText与响应体。
注意事项(Caveats)
- CORS 本质是浏览器侧的强制机制:被拒绝来源的非预检请求(例如简单请求)仍然会到达你的处理器。若 API 需要真正拒绝跨域调用,必须在处理器内部另行校验
Origin,不能只依赖 CORS 头。 credentials: true与origin: '*'组合时,中间件反射请求 Origin 并附加Vary: Origin,确保缓存安全。allowedHeaders为函数时,预检响应会基于Access-Control-Request-Headers变化(附加对应 Vary),避免缓存错配。preflightContinue与preflightStatusCode只影响预检OPTIONS请求的处理方式,不改变实际请求的鉴权逻辑。
相关包与延伸阅读
本包依赖fetch-router(Fetch API 路由核心,packages/fetch-router)与headers(类型化 HTTP 头工具,packages/headers),可结合以下仓库内容深入理解:
- cop-middleware:针对不安全跨源请求的浏览器来源保护中间件;
- fetch-router:Fetch API 路由器;
- headers:类型化 HTTP 头工具,其中的
Vary类是 CORS 缓存安全的关键基础设施。
CORS 协议本身的规范参考包括 MDN 的 Cross-Origin Resource Sharing 文档、WHATWG Fetch Standard 的 CORS protocol 章节,以及 expressjs/cors、rack-cors 等社区实现(本文不再展开外部链接)。本包遵循 MIT 许可,许可文本见仓库根目录 LICENSE。
总结
cors-middleware把 CORS 预检协议中最繁琐的部分(预检识别、Origin 匹配、凭据反射、Vary 缓存安全、私有网络预检)封装为一个声明式的cors()中间件,与fetch-router组合即可为 Fetch API 服务提供完整的跨域能力。无论你的场景是白名单 Origin、动态策略、携带凭据的会话 API,还是内网联调,都可以在 packages/cors-middleware/src/lib/cors.ts 与 packages/cors-middleware/src/lib/cors.test.ts 中找到对应的实现与测试依据,按需裁剪配置即可。
【免费下载链接】remixThe fully-stacked web framework项目地址: https://gitcode.com/GitHub_Trending/re/remix
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考