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
相关产品推荐
相关产品推荐

