本文记录一次真实项目(Nuxt 4 管理后台 + HttpOnly Cookie 会话 + 同源 /api/admin/**)中的鉴权代理问题排查结论,便于以后选型时不再踩「routeRules.proxy 看起来省事,但 Cookie 转不了 Authorization」的坑。
三种机制各自干什么
| 机制 | 配置位置 | 谁在执行 | 典型用途 |
|---|---|---|---|
routeRules.proxy |
nuxt.config.ts |
Nitro 内置反向代理(偏底层) | 零代码把某路径原样转到外部 URL |
server/middleware/* |
server/middleware/ |
Nitro 服务端中间件 | 改请求头、日志、鉴权注入(前提:请求先进入 Nitro 常规管线) |
server/api/* |
server/api/ |
Nitro 文件路由(BFF / 自定义代理) | 登录 BFF、带逻辑的 API 转发、改响应体 |
另外还有 middleware/*.global.ts(app/middleware):这是 Vue 路由中间件,只管页面导航(例如 /console 跳 /login),不参与 /api/admin/** 的 HTTP 代理。
调试前的设计:routeRules.proxy
// nuxt.config.ts(问题版本,已移除)
routeRules: {
'/api/admin/**': {
proxy: `${NUXT_API_PROXY_TARGET}/api/admin/**`
}
}
浏览器请求过程(简化):
sequenceDiagram participant Browser participant Nuxt as Nuxt_Nitro participant Proxy as routeRules_proxy participant Backend as 真实后端 Browser->>Nuxt: GET /api/admin/menus/user + Cookie Note over Nuxt: 期望 server/middleware 注入 Authorization Nuxt->>Proxy: 命中 routeRules.proxy Proxy->>Backend: 透明转发(常不带 Authorization) Backend-->>Browser: 401 未登录或 Token 已过期
日志里证实的问题
- 浏览器请求里 有
oa_authCookie。 - 前端
useClientApi确实请求http://localhost:3000/api/admin/menus/user。 server/middleware/proxy-auth.ts对/api/admin/menus/user无法完成有效鉴权(从原始Cookie头用getAuthHeaderFromCookieHeader解析时常失败;且整条链路可能不经过你期望的中间件逻辑)。- 后端返回:
{"code":401,"message":"未登录或 Token 已过期"}。
结论:routeRules.proxy 是透明转发,不会走 server/api/admin/[...path].ts,也不能指望 server/middleware 在代理前稳定地把 Cookie 转成 Authorization。
登录类请求:一直用 server/api
sequenceDiagram participant Browser participant BFF as server_api_auth participant ServerApi as createServerApi participant Backend as 真实后端 Browser->>BFF: POST /api/auth/login BFF->>ServerApi: 调 dingtalk/login 等 ServerApi->>Backend: 由 BFF 处理鉴权 Backend-->>BFF: access_token + user BFF->>Browser: Set-Cookie oa_auth + 只返回 user
server/api/auth/*:必须手写 BFF(写 HttpOnly Cookie、裁剪响应,不向浏览器暴露access_token)。createServerApi(event):直连NUXT_API_PROXY_TARGET,在beforeRequest里注入Authorization。- 这条链路 不依赖
routeRules.proxy,所以登录一直能工作。
调试后的业务 API:server/api/admin/[...path].ts
去掉 routeRules 的 /api/admin/** 代理后,由 catch-all 路由 接管:
sequenceDiagram
participant Browser
participant Handler as server_api_admin_path
participant Ky as createAdminProxyClient
participant Backend as 真实后端
Browser->>Handler: GET /api/admin/menus/user + Cookie
Handler->>Handler: getAuthHeaderFromEvent(event)
Note over Handler: Cookie 解析 + Bearer 规范化
Handler->>Ky: ky.get('menus/user')
Ky->>Backend: Authorization: Bearer xxx
Backend-->>Handler: 200 + JSON
Handler-->>Browser: 原样透传 body/status
关键实现点:
shared/auth-session.ts:Cookie 序列化/解析;将后端返回的小写bearer规范为Bearer;避免setCookie与手动encodeURIComponent双重编码。server/utils/createAdminProxyClient.ts:与 BFF 相同的 ky + 鉴权注入,但不抛业务错误(透传响应)。server/api/admin/[...path].ts:按 HTTP method 转发 body/query,设置响应status/content-type。
server/middleware/proxy-auth.ts 可保留作补充(给仍走 event.node.req 的 /api/admin 请求写 authorization),但 可靠路径是 handler + createAdminProxyClient。
页面侧:app/middleware + CSR
flowchart LR A[访问 /console/*] --> B[middleware/console-auth.global.ts] B -->|CSR| C[me 校验会话] B -->|SSR| D[只检查 oa_auth Cookie 是否存在] C --> E[console layout onMounted] E --> F[getCurrentUserMenus via useClientApi] F --> G[/api/admin/menus/user] G --> H[server/api/admin handler]
注意:
- 菜单拉取应在客户端(
onMounted/fetchMenus),避免 SSR 里 ky 对相对 URL 报Failed to parse URL from /api/admin/...。 useClientApi:credentials: 'include'带 Cookie;业务 401 时调/api/auth/logout再跳/login。
对比:该用哪种方式
| 需求 | routeRules.proxy |
server/middleware |
server/api |
|---|---|---|---|
| 零代码转发 | 适合 | 单独不够 | 可选 |
| Cookie → Authorization | 本次不可靠 | 仅当请求走常规 Nitro 管线 | 推荐 |
| 改登录响应 / 写 Cookie | 否 | 否 | server/api/auth |
| 统一透传 body/status | 否 | 否 | [...path].ts |
与 useClientApi 同源 /api/admin |
URL 可匹配 | 依赖是否进管线 | 明确命中 |
不要两套代理同时开
若同时保留:
routeRules: { '/api/admin/**': { proxy: '...' } }
和:
server/api/admin/[...path].ts
可能出现谁先匹配不确定、行为难排查。业务 API 应 只保留 handler 方案。
若坚持只用 routeRules.proxy
需要另找能在代理前注入鉴权的方式;按本次调试,仅靠 server/middleware + routeRules.proxy 不可靠。更稳妥的是:
- 登录 / 登出:
server/api/auth/* - 业务 CRUD:
server/api/admin/[...path].ts+createAdminProxyClient NUXT_API_PROXY_TARGET仍由backendAdminUrl()使用,只是不再写在routeRules里
一句话结论
routeRules.proxy:适合纯静态转发、不涉及会话改造的接口;不适合「HttpOnly Cookie → 后端 Bearer」的 BFF 场景。server/middleware:是 Nitro 钩子,不能假设所有/api/**都会经过它;routeRules.proxy往往会绕开。server/api:适合登录 BFF + 需要鉴权注入的业务代理;/api/auth/*与/api/admin/**([...path].ts)是正确分层。routeRules仍然有用:继续管页面级规则(如'/'的prerender、登录页ssr: false等),不要再和业务/api/admin/**的proxy混用。
相关环境变量(oa-web)
| 变量 | 作用 |
|---|---|
NUXT_PUBLIC_API_BASE_URL |
前端请求前缀,一般为 /api/admin/ |
NUXT_API_PROXY_TARGET |
服务端转发真实后端根地址 |
浏览器只访问同源 /api/admin/**;Nitro 在服务端把 Cookie 转为 Authorization 后转发到真实后端。