CORS 中间件
要点
- CORS(Cross-Origin Resource Sharing)解决浏览器跨域访问的安全问题
- Hono 内置的
cors()中间件处理预检请求和响应头设置 origin配置可以是字符串、字符串数组或函数,动态判断是否允许allowMethods、allowHeaders、exposeHeaders、maxAge控制预检缓存和头信息credentials: true允许携带 cookie,但origin不能是'*'- CORS 中间件必须放在鉴权中间件之前,否则 OPTIONS 请求会被 401 拦截
1. 为什么需要 CORS
浏览器的同源策略禁止网页向不同源的 API 发请求。所谓「同源」指协议、域名、端口完全一致。
https://app.example.com 上的前端访问 https://api.example.com/users,域名不同,属于跨域。浏览器默认会拦截这种请求的响应,除非服务端通过 CORS 响应头明确允许。
服务端需要做的事情:
- 响应普通的跨域请求,添加
Access-Control-Allow-Origin头 - 处理浏览器的 OPTIONS 预检请求,返回允许的方法、头、凭证等信息
Hono 的 cors() 中间件同时处理这两件事。
2. 基本用法
最简单的配置:允许所有来源的跨域请求。
// src/index.ts
import { Hono } from 'hono'
import { cors } from 'hono/cors'
const app = new Hono()
app.use('*', cors())
app.get('/api/users', (c) => c.json([{ id: 1, name: 'Alice' }]))
export default appcors() 不传参数时,默认允许所有来源、所有方法、所有头。响应的 CORS 头:
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, HEAD, PUT, POST, DELETE, PATCH
Vary: Origin这种配置适合开发阶段或完全公开的 API。生产环境通常需要限制来源。
3. 限制 origin
限制允许的来源,传入字符串或数组:
// src/index.ts
import { Hono } from 'hono'
import { cors } from 'hono/cors'
const app = new Hono()
// 单个来源
app.use('*', cors({
origin: 'https://app.example.com',
}))
export default app// 多个来源
app.use('*', cors({
origin: [
'https://app.example.com',
'https://admin.example.com',
'https://staging.example.com',
],
}))传入数组时,cors() 会检查请求的 Origin 头是否在数组里。如果在,响应头 Access-Control-Allow-Origin 设为该来源;如果不在,不设该头,浏览器会拒绝响应。
4. 动态 origin
有些场景需要在运行时判断是否允许某个来源,例如从数据库读取租户配置:
// src/index.ts
import { Hono } from 'hono'
import { cors } from 'hono/cors'
const app = new Hono()
app.use('*', cors({
origin: (origin) => {
// 允许 *.example.com 下的所有子域
if (origin.endsWith('.example.com')) {
return origin
}
// 其他来源返回 null,表示不允许
return null
},
}))
app.get('/api/users', (c) => c.json([]))
export default apporigin 函数接收请求的 Origin 头作为参数,返回允许的来源字符串或 null。返回 null 时,响应不会携带 Access-Control-Allow-Origin 头。
也可以用正则表达式匹配:
app.use('*', cors({
origin: (origin) => {
return /^https:\/\/.*\.example\.com$/.test(origin) ? origin : null
},
}))5. allowMethods 与 allowHeaders
allowMethods 控制预检响应里允许的 HTTP 方法:
app.use('*', cors({
origin: 'https://app.example.com',
allowMethods: ['GET', 'POST', 'PUT', 'DELETE'],
}))默认的 allowMethods 包含 GET, HEAD, PUT, POST, DELETE, PATCH。如果项目里不需要 PATCH,可以显式限制。
allowHeaders 控制预检响应里允许的请求头:
app.use('*', cors({
origin: 'https://app.example.com',
allowMethods: ['GET', 'POST', 'PUT', 'DELETE'],
allowHeaders: ['Content-Type', 'Authorization', 'X-Request-Id'],
}))如果前端请求带了 allowHeaders 里没有的头(例如 X-Custom-Header),浏览器会拒绝请求。
默认 allowHeaders 只包含 Content-Type。如果接口需要 Authorization 或其他自定义头,必须显式添加到 allowHeaders。
6. exposeHeaders
exposeHeaders 控制浏览器 JS 能读取哪些响应头。默认情况下,JS 只能读取少数几个简单响应头(Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma)。
如果服务端响应里带了 X-Request-Id、X-Total-Count 等自定义头,前端 JS 默认读不到。需要通过 exposeHeaders 暴露:
app.use('*', cors({
origin: 'https://app.example.com',
exposeHeaders: ['X-Request-Id', 'X-Total-Count', 'X-Response-Time'],
}))// 前端代码
const res = await fetch('https://api.example.com/users')
const requestId = res.headers.get('X-Request-Id') // 现在可以读到
const totalCount = res.headers.get('X-Total-Count') // 现在可以读到7. maxAge 与预检缓存
浏览器发跨域请求前,如果请求满足一定条件(非简单方法、非简单头、有自定义 Content-Type 等),会先发 OPTIONS 预检请求。预检请求的响应会缓存 maxAge 秒,期间不再发送预检。
app.use('*', cors({
origin: 'https://app.example.com',
maxAge: 86400, // 缓存 24 小时
}))预检响应头:
Access-Control-Max-Age: 86400设置合理的 maxAge 可以减少预检请求的数量,降低后端压力。常见值是 86400(24 小时)或 600(10 分钟)。
如果 CORS 配置变更后端没有立即生效,可能是浏览器缓存了旧的预检响应。清除浏览器缓存或等 maxAge 过期即可。
8. credentials 模式
如果跨域请求需要携带 cookie(例如用户登录态),需要同时满足:
- 前端
fetch()设置credentials: 'include' - 服务端 CORS 配置
credentials: true - 服务端
origin不能是'*',必须是具体的来源
// src/index.ts
import { Hono } from 'hono'
import { cors } from 'hono/cors'
const app = new Hono()
app.use('*', cors({
origin: 'https://app.example.com', // 不能是 '*'
credentials: true,
}))
app.get('/api/me', (c) => {
// 读取 cookie
const session = c.req.header('Cookie')
return c.json({ session })
})
export default app// 前端代码
await fetch('https://api.example.com/api/me', {
credentials: 'include', // 携带 cookie
})响应头:
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true如果 origin 设为 '*' 且 credentials: true,浏览器会报错。这是 W3C 规范的要求——允许任意来源携带凭证访问 API 存在安全风险。
9. CORS 与鉴权的顺序
CORS 中间件必须在鉴权中间件之前注册。原因在前面几节已经提过,这里再展开一下。
浏览器的 OPTIONS 预检请求不带 Authorization 头。如果鉴权中间件在 CORS 之前:
// ❌ 错误顺序
app.use('/api/*', async (c, next) => {
const token = c.req.header('Authorization')
if (!token) return c.json({ error: 'Unauthorized' }, 401)
await next()
})
app.use('*', cors())预检请求的处理流程:
- 浏览器发 OPTIONS 预检(不带 Authorization)
- 鉴权中间件拦截,返回 401
- 响应里没有 CORS 头
- 浏览器拒绝发送真正的请求
- 前端报 CORS 错误
正确顺序:
// ✅ 正确顺序
app.use('*', cors())
app.use('/api/*', async (c, next) => {
const token = c.req.header('Authorization')
if (!token) return c.json({ error: 'Unauthorized' }, 401)
await next()
})cors() 中间件检测到 OPTIONS 请求时,会直接响应预检结果,不调用 next()。这样鉴权中间件就不会接触到 OPTIONS 请求。
10. 排查 CORS 问题
CORS 问题的典型表现是前端报跨域错误,但后端 API 用 Postman 测试正常。排查方向:
- 检查请求方法:如果是 OPTIONS,说明是预检请求。看响应里的
Access-Control-Allow-*头是否完整 - 检查 origin:响应的
Access-Control-Allow-Origin是否匹配请求的Origin头 - 检查 allowHeaders:前端请求带了自定义头(例如 Authorization),服务端
allowHeaders里有没有包含 - 检查 credentials:前端设置了
credentials: 'include',服务端是否配置credentials: true且origin不是'*' - 检查中间件顺序:CORS 中间件是否在鉴权之前
浏览器 DevTools 的 Network 面板可以查看请求和响应的完整头信息。找到失败的请求,对比 Origin 和 Access-Control-Allow-Origin 就能定位大部分问题。
总结
CORS 中间件是前后端分离项目的必备组件。Hono 的 cors() 中间件处理了预检请求和响应头设置,配置项覆盖了大部分常见需求。
这一节涉及到的几个配置维度:
- origin:字符串、数组或函数,控制允许的来源
- allowMethods:预检允许的 HTTP 方法
- allowHeaders:预检允许的请求头
- exposeHeaders:前端 JS 可读取的响应头
- maxAge:预检缓存时间
- credentials:是否允许携带 cookie
CORS 问题通常不会在开发阶段暴露(前端和后端同源或禁用跨域检查),上线后才集中出现。理解浏览器跨域机制和中间件配置项,是预防这类问题的基础。
下一篇看 JWT 中间件——token 验证、cookie 与 header 双模式、refresh token 处理。