Chapter 04

Context 与认证

通过 Context 传递请求级状态,用 Middleware 构建类型安全的认证保护

4.1 Context 是什么

Context(上下文)是每个 tRPC 请求都会创建的对象,它携带请求级别的状态——用户信息、数据库连接、会话 token 等。所有 Procedure 的 handler 都可以通过 ctx 参数访问 Context。

Context 的创建发生在请求到达时,在任何 Procedure 执行之前。这是实现认证的关键切入点:从 HTTP 请求头中提取 token,验证用户身份,将用户信息注入 Context。

4.2 createContext() 函数

createContext() 接收原生 HTTP 请求对象,返回 Context 对象。对于每个请求,它都会被调用一次。

// server/context.ts
import { CreateNextContextOptions } from '@trpc/server/adapters/next';
import { getServerSession } from 'next-auth/next';
import { authOptions } from './auth';
import { db } from './db';

/**
 * 每个请求都会调用这个函数,创建 Context 对象
 * 这里从 Request 中提取用户 Session
 */
export async function createContext({ req, res }: CreateNextContextOptions) {
  const session = await getServerSession(req, res, authOptions);

  return {
    db,                // 数据库实例
    session,           // NextAuth session(未登录时为 null)
    req,               // 原始请求对象
  };
}

// 导出 Context 类型,供 initTRPC.context<Context>() 使用
export type Context = Awaited<ReturnType<typeof createContext>>;
TS

4.3 Middleware:t.middleware()

Middleware 是在 Procedure handler 执行之前运行的函数。它可以:

// server/trpc.ts — 认证 Middleware
const isAuthed = t.middleware(({ ctx, next }) => {
  // 检查 session 是否存在
  if (!ctx.session || !ctx.session.user) {
    throw new TRPCError({ code: 'UNAUTHORIZED', message: '请先登录' });
  }

  // 向 Context 追加 user 字段(类型从 session.user | null 变为 session.user)
  // 下游的 Procedure 可以安全地使用 ctx.user,不需要再判断 null
  return next({
    ctx: {
      ...ctx,
      session: ctx.session,         // 类型收窄
      user: ctx.session.user,       // 新增字段,类型安全
    },
  });
});

// 公开 Procedure(无需认证)
export const publicProcedure = t.procedure;

// 受保护 Procedure(需要登录)
export const protectedProcedure = t.procedure.use(isAuthed);
TS

使用 protectedProcedure

// 使用 protectedProcedure,TypeScript 知道 ctx.user 一定存在
getMyPosts: protectedProcedure
  .query(async ({ ctx }) => {
    // ctx.user.id 完全类型安全,不需要 ctx.user?.id
    return await ctx.db.post.findMany({
      where: { authorId: ctx.user.id },
    });
  }),
TS

4.4 Next.js App Router 的 Context

在 Next.js App Router 中,Context 的创建方式略有不同,使用 FetchCreateContextFnOptions

// server/context.ts(App Router 版本)
import { FetchCreateContextFnOptions } from '@trpc/server/adapters/fetch';
import { auth } from '@/lib/auth'; // NextAuth v5 auth()
import { db } from '@/lib/db';

export async function createContext({ req }: FetchCreateContextFnOptions) {
  const session = await auth();

  return {
    db,
    session,
    headers: Object.fromEntries(req.headers),
  };
}

export type Context = Awaited<ReturnType<typeof createContext>>;
TS

4.5 NextAuth.js 完整集成示例

// lib/auth.ts — NextAuth v5 配置
import NextAuth from 'next-auth';
import GitHub from 'next-auth/providers/github';
import Credentials from 'next-auth/providers/credentials';
import { z } from 'zod';

export const { handlers, auth, signIn, signOut } = NextAuth({
  providers: [
    GitHub({
      clientId: process.env.GITHUB_ID!,
      clientSecret: process.env.GITHUB_SECRET!,
    }),
    Credentials({
      async authorize(credentials) {
        const parsed = z.object({
          email: z.string().email(),
          password: z.string(),
        }).safeParse(credentials);
        if (!parsed.success) return null;
        // ... 验证密码逻辑
        return user;
      },
    }),
  ],
  callbacks: {
    async session({ session, token }) {
      // 将 userId 注入 session
      session.user.id = token.sub!;
      return session;
    },
  },
});
TS

4.6 JWT 认证(无 NextAuth 场景)

如果不使用 NextAuth,可以直接从 Authorization Header 中解析 JWT token:

// server/context.ts — JWT 认证版
import { verify } from 'jsonwebtoken';

export async function createContext({ req }: CreateNextContextOptions) {
  let user: { id: string; email: string } | null = null;

  const authHeader = req.headers.authorization;
  if (authHeader?.startsWith('Bearer ')) {
    const token = authHeader.slice(7);
    try {
      const payload = verify(token, process.env.JWT_SECRET!) as JWTPayload;
      user = { id: payload.sub, email: payload.email };
    } catch {
      // Token 无效,user 保持 null
    }
  }

  return { db, user };
}
TS

4.7 角色权限控制

基于 Middleware 可以实现细粒度的角色权限控制:

// 创建角色检查 Middleware 工厂函数
const requireRole = (role: 'admin' | 'moderator') =>
  t.middleware(({ ctx, next }) => {
    if (!ctx.user) {
      throw new TRPCError({ code: 'UNAUTHORIZED' });
    }
    if (ctx.user.role !== role && ctx.user.role !== 'admin') {
      throw new TRPCError({
        code: 'FORBIDDEN',
        message: `需要 ${role} 权限`,
      });
    }
    return next({ ctx });
  });

// 管理员专用 Procedure
export const adminProcedure = t.procedure
  .use(isAuthed)
  .use(requireRole('admin'));

// 使用示例
deleteUser: adminProcedure
  .input(z.object({ userId: z.string() }))
  .mutation(async ({ input, ctx }) => {
    return await ctx.db.user.delete({ where: { id: input.userId } });
  }),
TS
💡

本章小结Context 是 tRPC 请求的"环境",通过 createContext() 在请求开始时创建,包含数据库、用户 Session 等。Middleware 在 Procedure 前执行,可以修改 Context(类型安全地添加字段)或短路请求。protectedProcedure = t.procedure.use(isAuthed) 是实现认证保护的标准模式——一次定义,到处复用。