Next.js 13服务端组件如何传递Bearer Token?
问题
我通过客户端Firebase Auth获取了一个Bearer Token,并将其存储在标记为"use client"的AuthContext中。我的页面是Next.js 13的服务端组件,在该组件的getConversations异步方法中,需要向API发起fetch请求并携带此Bearer Token,请问如何获取该Token并完成请求?
提供的代码
AuthContext.tsx
// packages/webapp/src/context/AuthContext.tsx "use client" import React, { useEffect, useState, createContext, useContext, ReactNode } from 'react'; import { onAuthStateChanged, User, getAuth, getIdToken } from 'firebase/auth'; import firebaseApp from '@/lib/firebase'; const auth = getAuth(firebaseApp); interface IAuthContext { user: User | null; token: string | null; } export const AuthContext = createContext<IAuthContext>({ user: null, token: null }); export const useAuthContext = (): IAuthContext => useContext(AuthContext); interface IAuthContextProviderProps { children: ReactNode; } export const AuthContextProvider: React.FC<IAuthContextProviderProps> = ({ children }) => { const [user, setUser] = useState<User | null>(null); const [token, setToken] = useState<string | null>(null); const [loading, setLoading] = useState<boolean>(true); useEffect(() => { const unsubscribe = onAuthStateChanged(auth, async (user) => { if (user) { setUser(user); const userToken = await getIdToken(user, true); setToken(userToken); } else { setUser(null); setToken(null); } setLoading(false); }); return () => unsubscribe(); }, []); return ( <AuthContext.Provider value={{ user, token }}> {loading ? <div>Loading authentication...</div> : children} </AuthContext.Provider> ); };
页面组件page.tsx
// packages/webapp/src/app/(authenticated)/conversations/@list/page.tsx import React from 'react'; import { List, Box, TextField, IconButton, Typography, Divider } from '@mui/material'; import AddCircleOutlineIcon from '@mui/icons-material/AddCircleOutline'; import ConversationListPreview from '../_components/ConversationListPreview'; const getConversations = async () => { const response = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/api/conversations?limit=3`, { headers: { 'Authorization': `Bearer ${token}` } }); } export default async function ConversationListPage() { const conversations = await getConversations(); return ( <> <Box sx={{ width: '100%' }}> <Box display="flex" justifyContent="flex-start" mb={2}> <Typography variant="h6">Conversations</Typography> <IconButton color="primary"> <AddCircleOutlineIcon /> </IconButton> </Box> <TextField variant="outlined" fullWidth placeholder="Search..." size='small' /> </Box> <List sx={{ width: '100%', overflow: 'auto' }}> {conversations && conversations.map((conversation, index) => ( <> <ConversationListPreview key={`conversation-${conversation.id}`} conversation={conversation} /> {(index < (conversations.length - 1)) && <Divider />} </> ))} </List> </> ); };
解决方案
服务端组件无法直接访问客户端Context(Context是客户端运行时状态),因此需要通过Cookie中转Token让服务端读取,以下是具体实现步骤:
步骤1:在AuthContext中将Token写入Cookie
修改AuthContext.tsx,获取Token后同步写入Cookie,设置和Firebase Token匹配的过期时间(默认1小时):
// packages/webapp/src/context/AuthContext.tsx "use client" import React, { useEffect, useState, createContext, useContext, ReactNode } from 'react'; import { onAuthStateChanged, User, getAuth, getIdToken } from 'firebase/auth'; import firebaseApp from '@/lib/firebase'; const auth = getAuth(firebaseApp); interface IAuthContext { user: User | null; token: string | null; } export const AuthContext = createContext<IAuthContext>({ user: null, token: null }); export const useAuthContext = (): IAuthContext => useContext(AuthContext); interface IAuthContextProviderProps { children: ReactNode; } export const AuthContextProvider: React.FC<IAuthContextProviderProps> = ({ children }) => { const [user, setUser] = useState<User | null>(null); const [token, setToken] = useState<string | null>(null); const [loading, setLoading] = useState<boolean>(true); useEffect(() => { const unsubscribe = onAuthStateChanged(auth, async (user) => { if (user) { setUser(user); const userToken = await getIdToken(user, true); setToken(userToken); // 将Token写入Cookie,设置1小时过期 const expires = new Date(); expires.setTime(expires.getTime() + 60 * 60 * 1000); document.cookie = `firebase-token=${userToken}; expires=${expires.toUTCString()}; path=/; secure; SameSite=Strict`; } else { setUser(null); setToken(null); // 清除Cookie document.cookie = `firebase-token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`; } setLoading(false); }); return () => unsubscribe(); }, []); return ( <AuthContext.Provider value={{ user, token }}> {loading ? <div>Loading authentication...</div> : children} </AuthContext.Provider> ); };
步骤2:服务端组件读取Cookie并发起请求
修改page.tsx,通过Next.js的cookies()方法从服务端Cookie中提取Token,再携带到请求头:
// packages/webapp/src/app/(authenticated)/conversations/@list/page.tsx import React from 'react'; import { List, Box, TextField, IconButton, Typography, Divider } from '@mui/material'; import AddCircleOutlineIcon from '@mui/icons-material/AddCircleOutline'; import ConversationListPreview from '../_components/ConversationListPreview'; import { cookies } from 'next/headers'; const getConversations = async () => { // 从服务端Cookie获取Token const cookieStore = cookies(); const token = cookieStore.get('firebase-token')?.value; if (!token) { // 无Token时返回空数组或处理未授权逻辑 return []; } const response = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/api/conversations?limit=3`, { headers: { 'Authorization': `Bearer ${token}` }, // 根据需求设置缓存策略,no-store确保每次请求都获取最新数据 cache: 'no-store' }); if (!response.ok) { throw new Error('获取对话列表失败'); } return response.json(); } export default async function ConversationListPage() { const conversations = await getConversations(); return ( <> <Box sx={{ width: '100%' }}> <Box display="flex" justifyContent="flex-start" mb={2}> <Typography variant="h6">Conversations</Typography> <IconButton color="primary"> <AddCircleOutlineIcon /> </IconButton> </Box> <TextField variant="outlined" fullWidth placeholder="Search..." size='small' /> </Box> <List sx={{ width: '100%', overflow: 'auto' }}> {conversations && conversations.map((conversation, index) => ( <> <ConversationListPreview key={`conversation-${conversation.id}`} conversation={conversation} /> {(index < (conversations.length - 1)) && <Divider />} </> ))} </List> </> ); };
其他可选方案
方案1:改为客户端组件
如果页面不需要服务端渲染优势,可给page.tsx添加"use client",直接通过useAuthContext获取Token:
// page.tsx顶部添加 "use client" import { useAuthContext } from '@/context/AuthContext'; import { useState, useEffect } from 'react'; export default function ConversationListPage() { const { token } = useAuthContext(); const [conversations, setConversations] = useState([]); useEffect(() => { const fetchConversations = async () => { if (!token) return; const response = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/api/conversations?limit=3`, { headers: { 'Authorization': `Bearer ${token}` } }); const data = await response.json(); setConversations(data); }; fetchConversations(); }, [token]); // 其余代码不变 }
方案2:使用SWR/React Query
在客户端组件中用数据获取库管理请求,结合useAuthContext获取Token,兼顾缓存和状态管理。
内容的提问来源于stack exchange,提问作者Shamoon
相关产品推荐
相关产品推荐

