Agent X-Ray
RuntimeNotesAbout
Notes/代码工程/Encore/第10章

第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 三类用例)。

七、常见误区

  1. 无效凭据抛普通 Error 而不是 APIError.unauthenticated——会中止请求而非"视为未鉴权",公开端点也被误伤。
  2. 在公开端点 getAuthData()! 强制断言——未登录请求下是 null,运行时炸。
  3. 权限裁决(403)塞进 auth handler——handler 只回答"你是谁";"你能不能"留给端点/中间件。
  4. auth handler 里做重查询、调外部服务——它在每个带凭据的请求上执行,必须快;重上下文延迟到端点内加载。
  5. 以为内部调用不走鉴权语义——auth: true 端点的内部调用同样要求链路上有身份。
  6. 忘了 sensitive: true——登录、改密接口的载荷会进 trace(结合第 3 章)。
  7. 把 token 放查询串传输——进日志、进 Referer;凭据走 Header 或 Cookie。

八、实践练习

  1. 实现 UID 直登版鉴权(演示用):登录端点接收 UID 写会话表返回 token;auth handler 用 token 换 AuthData;/my/orders 声明 auth: true 按 UID 过滤。
  2. 把 handler 换成 JWT 版(jose + secret("JWTSecret")),保持 AuthData 不变,验证端点代码零改动——体会"验证方式与业务解耦"。
  3. 写一个 auth: false 的航班搜索端点:登录用户响应里多返回"常用乘机人"字段(getAuthData() 判空分支)。
  4. 加一个 role: "admin" 才能调的退款端点,分别用普通用户与管理员 token 验证 403/200。
  5. 为 auth handler 写三类单测:有效 / 过期 / 畸形凭据;为受保护端点写 mock getAuthData 的单测。

九、总结

  1. 三件套:authHandler<AuthParams, AuthData> 定义验证、Gateway 挂到网关层、auth: true 声明端点要求。
  2. 两张行为表背熟:无效凭据=视为未鉴权(不是中止);公开端点也能拿到鉴权数据。
  3. getAuthData() 取身份,跨服务自动透传,可显式覆盖;身份不会在链路中途凭空出现。
  4. JWT / API Key / Cookie 会话三模式共享同一骨架,换验证方式不动业务代码。
  5. handler 管"你是谁"(401),端点管"你能不能"(403);handler 必须快。
  6. 测试:mock getAuthData 测端点,直调函数测 handler。

请继续阅读:第11章:流式 API


原始资料引用



本章目录
一、三件套二、两张行为表三、在端点里取用户四、三种常见实现五、流式端点的鉴权六、测试鉴权代码七、常见误区八、实践练习九、总结原始资料引用Related Documents
苏ICP备2025204887号-2