You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Next.js + tRPC JWT认证问题:客户端与服务端token及用户对象不同步

解决方案

问题1:服务端tRPC流程中获取access token

场景1:客户端发起的tRPC请求(CSR)

在客户端配置tRPC的http链接时,自动从Zustand状态中读取access token并添加到请求头:

import { httpBatchLink } from '@trpc/client';
import { useAuthStore } from '@/stores/auth';

const trpcClient = createTRPCNext<AppRouter>({
  config() {
    return {
      links: [
        httpBatchLink({
          url: '/api/trpc',
          // 每次请求携带access token到请求头
          headers() {
            const { accessToken } = useAuthStore.getState();
            return {
              Authorization: accessToken ? `Bearer ${accessToken}` : '',
            };
          },
        }),
      ],
    };
  },
});

然后在服务端的createTRPCContext中,从请求头提取token并注入context:

export async function createTRPCContext(opts: { req: NextApiRequest; res: NextApiResponse }) {
  const { req, res } = opts;
  // 从Authorization头解析access token
  const authHeader = req.headers.authorization;
  const accessToken = authHeader?.startsWith('Bearer ') ? authHeader.slice(7) : null;

  return {
    req,
    res,
    accessToken,
    // 同时从cookie中拿到refresh token备用
    refreshToken: req.cookies.refreshToken,
  };
}

之后tRPC的procedure就能直接从context中获取accessToken用于调用Express后端接口。

场景2:服务端渲染/API路由中的tRPC调用(SSR/SSG)

此时客户端Zustand状态未初始化,需要用cookie中的refresh token主动换取access token:

// 在createTRPCContext中扩展逻辑
export async function createTRPCContext(opts: { req: NextApiRequest; res: NextApiResponse }) {
  const { req, res } = opts;
  let accessToken = null;
  let user = null;
  const refreshToken = req.cookies.refreshToken;

  if (refreshToken) {
    // 调用Express的refresh接口获取新的access token和用户信息
    try {
      const refreshRes = await fetch('http://your-express-backend/refresh', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        credentials: 'include', // 携带refresh token的cookie
        body: JSON.stringify({ refreshToken }),
      });
      const data = await refreshRes.json();
      accessToken = data.accessToken;
      user = data.user;
    } catch (err) {
      // 处理refresh失败的情况(如token过期)
      console.error('Refresh token failed:', err);
    }
  }

  return {
    req,
    res,
    accessToken,
    refreshToken,
    user,
  };
}

如果是SSR页面,可以将context中的accessToken和user通过props传递给客户端,初始化Zustand状态:

export async function getServerSideProps(context) {
  const trpcContext = await createTRPCContext(context);
  const { accessToken, user } = trpcContext;

  return {
    props: {
      initialAuthState: { accessToken, user },
    },
  };
}

客户端页面中初始化Zustand:

export default function Page({ initialAuthState }) {
  const authStore = useAuthStore();
  useEffect(() => {
    if (initialAuthState.accessToken) {
      authStore.setState(initialAuthState);
    }
  }, [initialAuthState]);

  // ...页面内容
}

问题2:服务端刷新token后更新客户端Zustand状态

服务端无法直接修改客户端状态,需通过响应头传递新token+客户端拦截响应更新状态的方式实现:

步骤1:服务端tRPC procedure中处理token过期逻辑

在调用Express接口时捕获401错误,用refresh token换取新的access token,并重试请求,同时将新token和用户信息添加到响应头:

import { initTRPC } from '@trpc/server';
import { createTRPCContext } from './context';
import { TRPCError } from '@trpc/server';

const t = initTRPC.context<typeof createTRPCContext>().create();

export const protectedProcedure = t.procedure.use(async ({ ctx, next }) => {
  if (!ctx.accessToken) {
    throw new TRPCError({ code: 'UNAUTHORIZED' });
  }

  try {
    // 调用Express后端接口
    return await next();
  } catch (err) {
    // 检测是否为access token过期的401
    if (err instanceof Error && 'response' in err && err.response?.status === 401 && ctx.refreshToken) {
      // 刷新token
      const refreshRes = await fetch('http://your-express-backend/refresh', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        credentials: 'include',
        body: JSON.stringify({ refreshToken: ctx.refreshToken }),
      });

      if (!refreshRes.ok) {
        throw new TRPCError({ code: 'UNAUTHORIZED', message: 'Refresh token expired' });
      }

      const { accessToken, user } = await refreshRes.json();
      // 将新token和用户信息添加到响应头
      ctx.res.setHeader('x-new-access-token', accessToken);
      ctx.res.setHeader('x-new-user-info', JSON.stringify(user));
      // 更新context中的token,重试请求
      ctx.accessToken = accessToken;
      ctx.user = user;
      return await next();
    }
    throw err;
  }
});

步骤2:客户端拦截tRPC响应并更新Zustand

在tRPC的http链接中添加onResponse钩子,读取响应头中的新token并更新Zustand状态:

const httpLink = httpBatchLink({
  url: '/api/trpc',
  headers() { /* 如上 */ },
  async onResponse(opts) {
    const newToken = opts.response.headers.get('x-new-access-token');
    const newUserStr = opts.response.headers.get('x-new-user-info');
    
    if (newToken && newUserStr) {
      const newUser = JSON.parse(newUserStr);
      useAuthStore.setState({
        accessToken: newToken,
        user: newUser,
      });
    }
  },
});

补充:SSR场景下的状态同步

如果是SSR页面,服务端刷新token后,直接将新的accessToken和user通过页面props传递给客户端,客户端在初始化时覆盖Zustand状态即可(同问题1中的SSR处理)。


内容的提问来源于stack exchange,提问作者Murtaza

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 15:01:01