第10章:鉴权 —— authHandler 与 Gateway
约 4 分钟 · 更新于 2026-09-02
Encore 的鉴权是"三件套 + 两张行为表":authHandler 定义怎么验,Gateway 让它在网关层执行,auth: true 声明哪些端点需要。本章给出 JWT、API Key、Cookie 会话三种完整实现与测试方法。
一、三件套
typescript
// auth/auth.ts
import { Header, Gateway, APIError } from "encore.dev/api";
import { authHandler } from "encore.dev/auth";
// ① 鉴权处理器从请求里读什么
interface AuthParams {
authorization: Header<"Authorization">;
}
// ② 验完往下游传什么
interface AuthData {
userID: string;
email: string;
role: "admin" | "user";
}
// ③ 处理器本体
export const auth = authHandler<AuthParams, AuthData>(async (params) => {
const token = params.authorization.replace("Bearer ", "");
const payload = await verifyToken(token);
if (!payload) {
throw APIError.unauthenticated("invalid token");
}
return { userID: payload.sub, email: payload.email, role: payload.role };
});
// ④ 注册到网关:所有携带鉴权参数的请求都会先过它
export const gateway = new Gateway({ authHandler: auth });
AuthParams 与普通请求接口一样支持 Header<> / Query<> / Cookie<>——鉴权凭据可以来自请求头、查询串或 Cookie。
执行位置值得注意:Gateway 内嵌在 Rust 运行时里(Pingora),auth handler 是在网关层同步执行的 TypeScript——没有独立鉴权服务的网络一跳。
二、两张行为表
处理器行为
| 场景 | 处理器动作 | 结果 |
|---|
| 凭据有效 | 返回 AuthData | 请求带鉴权身份继续 |
| 凭据无效 | 抛 APIError.unauthenticated() | 视为"未携带鉴权"继续走(到端点再裁决) |
| 其他异常 | 抛其他错误 | 请求直接中止 |
端点行为
| 端点配置 | 请求带有效鉴权 | 请求无鉴权 |
|---|
| auth: true | 执行,getAuthData() 非空 | 401 Unauthenticated |
| auth: false / 省略 | 执行,鉴权数据依然可用 | 执行,无鉴权数据 |
第二行的细节容易被忽略:公开端点也能拿到鉴权数据(如果请求带了有效凭据)——"登录与未登录展示不同内容"的端点就该这样写:auth: false + getAuthData() 判空。
三、在端点里取用户
typescript
import { api } from "encore.dev/api";
import { getAuthData } from "~encore/auth";
export const myOrders = api(
{ method: "GET", path: "/my/orders", expose: true, auth: true },
async (): Promise<OrderList> => {
const auth = getAuthData()!; // auth: true 下必非空
return listOrdersByUser(auth.userID);
},
);
getAuthData() 类型即你定义的 AuthData;在 auth: true 端点里断言非空是安全的,在公开端点里必须判空。
跨服务透传与覆盖
typescript
import { user } from "~encore/clients";
// 默认:当前请求的鉴权数据自动传给内部调用
const profile = await user.getProfile();
// 显式覆盖(如系统任务以特定身份执行)
const adminProfile = await user.getProfile(
{},
{ authData: { userID: "admin-1", email: "ops@example.com", role: "admin" } },
);
框架保证一条链式性质:调用 auth: true 端点的内部调用,若原始请求没有鉴权,调用会失败——身份不会在调用链中途凭空出现。
四、三种常见实现
JWT(无状态令牌)
typescript
import { jwtVerify } from "jose";
import { secret } from "encore.dev/config";
const jwtSecret = secret("JWTSecret");
async function verifyToken(token: string) {
try {
const { payload } = await jwtVerify(token, new TextEncoder().encode(jwtSecret()));
return payload;
} catch {
return null;
}
}
API Key(服务对服务 / 开放平台)
typescript
export const auth = authHandler<AuthParams, AuthData>(async (params) => {
const apiKey = params.authorization;
const caller = await db.queryRow<Caller>`
SELECT id, name, role FROM api_callers WHERE api_key = ${apiKey} AND enabled = true
`;
if (!caller) throw APIError.unauthenticated("invalid API key");
return { userID: caller.id, email: "", role: caller.role };
});
Cookie 会话(自建会话表)
typescript
interface AuthParams {
cookie: Header<"Cookie">;
}
export const auth = authHandler<AuthParams, AuthData>(async (params) => {
const sessionId = parseCookie(params.cookie, "session");
if (!sessionId) throw APIError.unauthenticated("no session");
const session = await getSession(sessionId);
if (!session || session.expiresAt < new Date()) {
throw APIError.unauthenticated("session expired");
}
return { userID: session.userID, email: session.email, role: session.role };
});
本机 B2B 项目的组合拳
真实项目往往混合:登录端点(公开)发短信/校验账密 → 写会话表 → 返回 Bearer token;auth handler 查会话表换 AuthData(含 UID 与分销商上下文);下游按 UID 取各自的分销凭据调上游。鉴权处理器保持"快"(一次索引查询),重的上下文加载放到端点内按需做。
角色权限 (RBAC) 的裁决放端点内:if (auth.role !== "admin") throw APIError.permissionDenied(...)——401 是"你是谁不知道",403 是"知道你是谁但不许"。
五、流式端点的鉴权
流式 API(第 11 章)与普通端点一致:选项加 auth: true,handler 里 getAuthData()。鉴权发生在 WebSocket 握手阶段。
六、测试鉴权代码
单测里 mock getAuthData:
typescript
import { describe, it, expect, vi } from "vitest";
import * as auth from "~encore/auth";
import { myOrders } from "./api";
describe("myOrders", () => {
it("returns orders for the authenticated user", async () => {
const spy = vi.spyOn(auth, "getAuthData");
spy.mockImplementation(() => ({
userID: "user-42", email: "t@example.com", role: "user",
}));
const result = await myOrders();
expect(result.orders.every(o => o.userId === "user-42")).toBe(true);
spy.mockRestore();
});
});
auth handler 本身是普通异步函数——直接传参调用测试(有效 token、过期 token、畸形 token 三类用例)。
七、常见误区
- 无效凭据抛普通 Error 而不是 APIError.unauthenticated——会中止请求而非"视为未鉴权",公开端点也被误伤。
- 在公开端点 getAuthData()! 强制断言——未登录请求下是 null,运行时炸。
- 权限裁决(403)塞进 auth handler——handler 只回答"你是谁";"你能不能"留给端点/中间件。
- auth handler 里做重查询、调外部服务——它在每个带凭据的请求上执行,必须快;重上下文延迟到端点内加载。
- 以为内部调用不走鉴权语义——auth: true 端点的内部调用同样要求链路上有身份。
- 忘了 sensitive: true——登录、改密接口的载荷会进 trace(结合第 3 章)。
- 把 token 放查询串传输——进日志、进 Referer;凭据走 Header 或 Cookie。
八、实践练习
- 实现 UID 直登版鉴权(演示用):登录端点接收 UID 写会话表返回 token;auth handler 用 token 换 AuthData;/my/orders 声明 auth: true 按 UID 过滤。
- 把 handler 换成 JWT 版(jose + secret("JWTSecret")),保持 AuthData 不变,验证端点代码零改动——体会"验证方式与业务解耦"。
- 写一个 auth: false 的航班搜索端点:登录用户响应里多返回"常用乘机人"字段(getAuthData() 判空分支)。
- 加一个 role: "admin" 才能调的退款端点,分别用普通用户与管理员 token 验证 403/200。
- 为 auth handler 写三类单测:有效 / 过期 / 畸形凭据;为受保护端点写 mock getAuthData 的单测。
九、总结
- 三件套:authHandler<AuthParams, AuthData> 定义验证、Gateway 挂到网关层、auth: true 声明端点要求。
- 两张行为表背熟:无效凭据=视为未鉴权(不是中止);公开端点也能拿到鉴权数据。
- getAuthData() 取身份,跨服务自动透传,可显式覆盖;身份不会在链路中途凭空出现。
- JWT / API Key / Cookie 会话三模式共享同一骨架,换验证方式不动业务代码。
- handler 管"你是谁"(401),端点管"你能不能"(403);handler 必须快。
- 测试:mock getAuthData 测端点,直调函数测 handler。
请继续阅读:第11章:流式 API。
原始资料引用