Skip to content

CORS

@vafast/cors 为响应补充 跨源资源共享 相关头,并可自动处理 OPTIONS 预检(默认开启)。

默认配置较宽松(允许任意 Origin、反射方法 / 头、允许 credentials),生产环境请按域名收紧。

先搞清几个概念(给新用户)

浏览器的同源策略默认禁止网页随便读另一个源(协议 / 主机 / 端口不同)的响应。CORS 是服务端通过响应头「声明哪些跨源访问是允许的」的机制。

名词白话
Origin(源)https://app.example.com:443。前端页面所在源会出现在请求头 Origin
简单请求部分 GET/POST 等在限定条件下可直接发;浏览器仍会检查响应里的 CORS 头是否允许前端读结果
预检(preflight)浏览器先发一次 OPTIONS,问「能不能用这些方法/头跨域?」;通过后再发真正的 POST/PUT 等。本包默认拦截 OPTIONS 并返回 204
Access-Control-Allow-Origin(ACAO)告诉浏览器:哪个源可以读这个响应。可以是具体源,或(无 credentials 时)*
credentials跨域请求是否带 Cookie、HTTP 认证、TLS 客户端证书等。前端需 fetch(..., { credentials: 'include' }),服务端需 Access-Control-Allow-Credentials: true
为何 credentials 不能配 ACAO *浏览器规范:带凭证时,Access-Control-Allow-Origin 必须是具体源,不能是通配 *。本包默认 origin: true回显请求的 Origin,以便与默认 credentials: true 兼容
Access-Control-Allow-Methods / -Headers预检时声明允许的方法、请求头
Access-Control-Expose-Headers声明前端 JS 可以读取哪些响应头(默认很多头对跨域脚本不可见)
Access-Control-Max-Age预检结果缓存秒数,减少反复 OPTIONS

安装

bash
npm install @vafast/cors

快速开始

typescript
import { Server, defineRoute, defineRoutes, json, serve } from 'vafast'
import { cors } from '@vafast/cors'

const routes = defineRoutes([
  defineRoute({
    method: 'GET',
    path: '/',
    handler: () => json({ ok: true }),
  }),
])

const server = new Server(routes)
server.use(cors())

serve({ fetch: server.fetch, port: 3000 })

用法

基础用法

推荐全局挂载,这样业务响应与错误响应都会带上 CORS 头(洋葱模型下,next() 之后统一后处理)。

typescript
server.use(cors())

也可挂在单条路由的 middleware 上。

常见场景

1. 允许指定前端域名

typescript
server.use(
  cors({
    origin: ['https://example.com', 'https://app.example.com'],
    credentials: true,
  }),
)

匹配成功时,Access-Control-Allow-Origin 设为请求的 Origin

2. 用正则放行子域

typescript
server.use(
  cors({
    origin: /https:\/\/.*\.example\.com$/,
  }),
)

正则对请求头 OriginRegExp.test

3. 函数动态判断

typescript
server.use(
  cors({
    origin: (request) => {
      const origin = request.headers.get('Origin')
      return origin?.endsWith('.example.com') ?? false
    },
  }),
)

函数必须 显式返回 true 才放行(返回 void / false 都不行)。

4. 限制方法与请求头

typescript
server.use(
  cors({
    origin: 'https://example.com',
    methods: ['GET', 'POST', 'PUT'],
    allowedHeaders: ['Content-Type', 'Authorization'],
    exposeHeaders: ['X-Request-Id'],
    maxAge: 600,
  }),
)

5. 关闭自动预检

若你自己实现 OPTIONS 路由,可关掉:

typescript
server.use(cors({ preflight: false }))

API

导出

导出说明
cors工厂函数,返回中间件
defaultcors
HTTPMethod允许的方法字面量类型(含 GET / POST / OPTIONS 等标准与扩展方法名)

选项 / 参数

typescript
cors(config?: CORSConfig)
参数类型默认说明
originboolean | string | RegExp | ((request: Request) => boolean | void) | Array<string | RegExp | 函数>true控制 Access-Control-Allow-Origin / Vary。详见下方 origin 行为
methodsboolean | null | '' | '*' | HTTPMethod | string | 数组trueAccess-Control-Allow-Methods。详见 methods 行为
allowedHeaderstrue | string | string[]trueAccess-Control-Allow-Headers。数组会先 join(', ')
exposeHeaderstrue | string | string[]trueAccess-Control-Expose-Headers。数组会先 join(', ')
credentialsbooleantruetrue 时写 Access-Control-Allow-Credentials: true(预检与实际请求都会写)
maxAgenumber设置后写入 Access-Control-Max-Age未传则不写该头
preflightbooleantruetrue 时拦截 OPTIONS,直接返回 204 + CORS 头,不再进入后续路由

origin 行为(与源码一致)

行为
true(默认)设置 Vary: *Access-Control-Allow-Origin = 请求的 Origin,若无 Origin 则为 *
字符串与请求 Origin 精确匹配;也支持去掉 :// 后的 host 与配置字符串比较。匹配则回显该 Origin,并 Vary: Origin(有 origin 配置时)
数组中的字符串同上;任一字符串命中即放行
'*'(出现在列表中,使 anyOrigin 为真)设为 Access-Control-Allow-Origin: *,并 Vary: *credentials: true 组合时,浏览器通常会拒绝带 Cookie 的跨域响应
RegExp对请求头 Origintest;命中则回显该 Origin
Function入参为 Request;返回值严格等于 true 时放行并回显 Originvoid / false 均不放行
数组(混合)按上列规则依次尝试,任一命中即放行
false / 空数组等导致无可用规则可能不设置有效的 ACAO(跨域前端读不到响应)

类型注释里「origin: true 等于设为 *不准确:实现是 回显请求 Origin(无 Origin 时才是 *),以便与默认 credentials: true 兼容。以本节表格为准。

methods 行为

行为
true(默认)预检时镜像请求头 Access-Control-Request-Method;实际请求镜像当前 request.method
'*'固定写入 Access-Control-Allow-Methods: *
单个方法字符串 / 逗号分隔字符串直接写入
方法数组join(', ') 后写入
false / null / '' / 空数组不设置该头

allowedHeaders / exposeHeaders

行为
true(默认)预检allowedHeaders 镜像 Access-Control-Request-HeadersexposeHeaders 取当前请求头名列表。实际请求:两者都取当前请求头名列表
字符串直接写入对应响应头
字符串数组先 join 成逗号分隔字符串再写入

相关方法

无额外辅助函数。预检响应使用框架的 empty(204)

中间件内部有一处预检辅助逻辑与正式分支等价;对外只暴露 cors()

最佳实践

  1. 生产把 origin 收成白名单(字符串数组或函数),避免默认「任意源 + credentials」
  2. 需要带 Cookie 跨域时:credentials: true,且 ACAO 必须是具体源——用白名单或默认的「回显 Origin」;不要用列表里的 '*'
  3. 前端配合:fetch(url, { credentials: 'include' })(或 axios withCredentials: true),否则浏览器不会带 Cookie
  4. 明确列出 methods / allowedHeaders,减少预检缓存与安全面
  5. 合理设置 maxAge,降低预检频率
  6. 若响应里有自定义头要给前端读(如 X-Request-Id),务必写进 exposeHeaders

注意事项

  • preflight: true 时,所有 OPTIONS 由本中间件直接结束,不会进入后续路由
  • maxAge 的 JSDoc 曾写默认 5源码未设默认值:只有传入数字才会写头
  • 实际请求路径不吞掉 handler 抛错,CORS 头会加在 errorHandler 返回的响应上(只要仍经过此后处理)
  • credentials: true 时,浏览器要求 ACAO 为具体源;若你强制 origin: '*'(或数组含 '*' 触发 anyOrigin),可能与 credentials 冲突
  • 本地调试时注意前端源(如 http://localhost:5173)与白名单字符串必须完全一致(含协议与端口)

相关链接