🙋 用户登录后如何保持状态?如何保护需要登录才能访问的页面?
认证方式对比#
| 方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| Session | 安全、可撤销 | 需要存储、难扩展 | 传统应用 |
| JWT | 无状态、易扩展 | 难撤销、体积大 | API、微服务 |
| Cookie | 自动发送、HttpOnly | 大小限制 | 与 Session/JWT 配合 |
Cookie 基础#
设置 Cookie#
import { NextRequest, NextResponse } from 'next/server'import { cookies } from 'next/headers'
export async function POST(request: NextRequest) { const { email, password } = await request.json()
// 验证用户... const user = await authenticateUser(email, password)
if (!user) { return NextResponse.json({ error: '登录失败' }, { status: 401 }) }
// 设置 Cookie const cookieStore = await cookies() cookieStore.set('session', user.sessionId, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax', maxAge: 60 * 60 * 24 * 7, // 7 天 path: '/', })
return NextResponse.json({ user: { id: user.id, name: user.name } })}读取 Cookie#
import { NextResponse } from 'next/server'import { cookies } from 'next/headers'
export async function GET() { const cookieStore = await cookies() const session = cookieStore.get('session')
if (!session) { return NextResponse.json({ error: '未登录' }, { status: 401 }) }
const user = await getUserBySession(session.value)
if (!user) { return NextResponse.json({ error: '会话无效' }, { status: 401 }) }
return NextResponse.json({ user })}删除 Cookie#
import { NextResponse } from 'next/server'import { cookies } from 'next/headers'
export async function POST() { const cookieStore = await cookies() cookieStore.delete('session')
return NextResponse.json({ success: true })}Session 认证#
创建 Session#
import { cookies } from 'next/headers'import { SignJWT, jwtVerify } from 'jose'
const secretKey = new TextEncoder().encode(process.env.SESSION_SECRET!)
interface SessionPayload { userId: string expiresAt: Date}
export async function createSession(userId: string) { const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) // 7 天
const session = await new SignJWT({ userId, expiresAt }) .setProtectedHeader({ alg: 'HS256' }) .setExpirationTime('7d') .sign(secretKey)
const cookieStore = await cookies() cookieStore.set('session', session, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax', expires: expiresAt, path: '/', })}
export async function verifySession(): Promise<SessionPayload | null> { const cookieStore = await cookies() const session = cookieStore.get('session')?.value
if (!session) return null
try { const { payload } = await jwtVerify(session, secretKey) return payload as SessionPayload } catch { return null }}
export async function deleteSession() { const cookieStore = await cookies() cookieStore.delete('session')}登录流程#
'use server'
import { z } from 'zod'import { createSession, deleteSession } from '@/lib/session'import { redirect } from 'next/navigation'import { hash, compare } from 'bcryptjs'
const loginSchema = z.object({ email: z.string().email(), password: z.string().min(8),})
export async function login(prevState: any, formData: FormData) { const result = loginSchema.safeParse({ email: formData.get('email'), password: formData.get('password'), })
if (!result.success) { return { error: '请输入有效的邮箱和密码' } }
const { email, password } = result.data
const user = await db.user.findUnique({ where: { email } })
if (!user || !(await compare(password, user.password))) { return { error: '邮箱或密码错误' } }
await createSession(user.id) redirect('/dashboard')}
export async function logout() { await deleteSession() redirect('/login')}
export async function register(prevState: any, formData: FormData) { const email = formData.get('email') as string const password = formData.get('password') as string const name = formData.get('name') as string
const hashedPassword = await hash(password, 12)
try { const user = await db.user.create({ data: { email, password: hashedPassword, name }, })
await createSession(user.id) redirect('/dashboard') } catch { return { error: '注册失败,邮箱可能已被使用' } }}JWT 认证#
生成 Token#
import { SignJWT, jwtVerify } from 'jose'
const secretKey = new TextEncoder().encode(process.env.JWT_SECRET!)
interface TokenPayload { userId: string email: string}
export async function generateToken(payload: TokenPayload): Promise<string> { return new SignJWT(payload) .setProtectedHeader({ alg: 'HS256' }) .setIssuedAt() .setExpirationTime('1h') .sign(secretKey)}
export async function generateRefreshToken(userId: string): Promise<string> { return new SignJWT({ userId }) .setProtectedHeader({ alg: 'HS256' }) .setIssuedAt() .setExpirationTime('7d') .sign(secretKey)}
export async function verifyToken(token: string): Promise<TokenPayload | null> { try { const { payload } = await jwtVerify(token, secretKey) return payload as TokenPayload } catch { return null }}Token 刷新#
import { NextRequest, NextResponse } from 'next/server'import { cookies } from 'next/headers'import { verifyToken, generateToken, generateRefreshToken } from '@/lib/jwt'
export async function POST(request: NextRequest) { const cookieStore = await cookies() const refreshToken = cookieStore.get('refreshToken')?.value
if (!refreshToken) { return NextResponse.json({ error: 'No refresh token' }, { status: 401 }) }
const payload = await verifyToken(refreshToken)
if (!payload) { return NextResponse.json( { error: 'Invalid refresh token' }, { status: 401 } ) }
const user = await db.user.findUnique({ where: { id: payload.userId } })
if (!user) { return NextResponse.json({ error: 'User not found' }, { status: 401 }) }
// 生成新 token const accessToken = await generateToken({ userId: user.id, email: user.email, }) const newRefreshToken = await generateRefreshToken(user.id)
// 更新 refresh token cookie cookieStore.set('refreshToken', newRefreshToken, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax', maxAge: 60 * 60 * 24 * 7, path: '/', })
return NextResponse.json({ accessToken })}路由保护#
中间件保护#
import { NextResponse } from 'next/server'import type { NextRequest } from 'next/server'import { verifySession } from '@/lib/session'
// 需要登录的路由const protectedRoutes = ['/dashboard', '/settings', '/profile']// 登录后不能访问的路由const authRoutes = ['/login', '/register']
export async function middleware(request: NextRequest) { const path = request.nextUrl.pathname const isProtectedRoute = protectedRoutes.some((route) => path.startsWith(route) ) const isAuthRoute = authRoutes.some((route) => path.startsWith(route))
const session = await verifySession()
// 未登录访问受保护路由 if (isProtectedRoute && !session) { return NextResponse.redirect(new URL('/login', request.url)) }
// 已登录访问登录页 if (isAuthRoute && session) { return NextResponse.redirect(new URL('/dashboard', request.url)) }
return NextResponse.next()}
export const config = { matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],}服务端组件保护#
import { redirect } from 'next/navigation'import { verifySession } from '@/lib/session'
export default async function DashboardPage() { const session = await verifySession()
if (!session) { redirect('/login') }
const user = await db.user.findUnique({ where: { id: session.userId }, })
return ( <div> <h1>欢迎,{user?.name}</h1> </div> )}封装认证检查#
import { verifySession } from './session'import { redirect } from 'next/navigation'import { cache } from 'react'
export const getUser = cache(async () => { const session = await verifySession()
if (!session) { return null }
return db.user.findUnique({ where: { id: session.userId }, select: { id: true, name: true, email: true, role: true, }, })})
export async function requireAuth() { const user = await getUser()
if (!user) { redirect('/login') }
return user}
export async function requireAdmin() { const user = await requireAuth()
if (user.role !== 'ADMIN') { redirect('/unauthorized') }
return user}使用:
import { requireAdmin } from '@/lib/auth'
export default async function AdminPage() { const admin = await requireAdmin()
return <div>管理员页面,{admin.name}</div>}客户端认证状态#
Context Provider#
'use client'
import { createContext, useContext, useState, useEffect, ReactNode,} from 'react'
interface User { id: string name: string email: string}
interface AuthContextType { user: User | null loading: boolean login: (email: string, password: string) => Promise<void> logout: () => Promise<void>}
const AuthContext = createContext<AuthContextType | null>(null)
export function AuthProvider({ children }: { children: ReactNode }) { const [user, setUser] = useState<User | null>(null) const [loading, setLoading] = useState(true)
useEffect(() => { checkAuth() }, [])
async function checkAuth() { try { const res = await fetch('/api/me') if (res.ok) { const data = await res.json() setUser(data.user) } } catch { setUser(null) } finally { setLoading(false) } }
async function login(email: string, password: string) { const res = await fetch('/api/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, password }), })
if (!res.ok) { throw new Error('登录失败') }
const data = await res.json() setUser(data.user) }
async function logout() { await fetch('/api/logout', { method: 'POST' }) setUser(null) }
return ( <AuthContext.Provider value={{ user, loading, login, logout }}> {children} </AuthContext.Provider> )}
export function useAuth() { const context = useContext(AuthContext) if (!context) { throw new Error('useAuth must be used within AuthProvider') } return context}使用 Hook#
'use client'
import { useAuth } from '@/context/auth'import Link from 'next/link'
export function Header() { const { user, loading, logout } = useAuth()
if (loading) { return <div>加载中...</div> }
return ( <header className="flex justify-between items-center p-4"> <Link href="/">Logo</Link>
<nav> {user ? ( <div className="flex items-center gap-4"> <span>欢迎,{user.name}</span> <button onClick={logout}>退出</button> </div> ) : ( <div className="flex gap-4"> <Link href="/login">登录</Link> <Link href="/register">注册</Link> </div> )} </nav> </header> )}登录表单#
'use client'
import { useActionState } from 'react'import { login } from '@/app/actions/auth'
export function LoginForm() { const [state, formAction, isPending] = useActionState(login, null)
return ( <form action={formAction} className="space-y-4 max-w-md"> <div> <label htmlFor="email" className="block font-medium"> 邮箱 </label> <input id="email" name="email" type="email" required className="w-full border rounded px-3 py-2" /> </div>
<div> <label htmlFor="password" className="block font-medium"> 密码 </label> <input id="password" name="password" type="password" required className="w-full border rounded px-3 py-2" /> </div>
{state?.error && <p className="text-red-500">{state.error}</p>}
<button type="submit" disabled={isPending} className="w-full bg-blue-600 text-white py-2 rounded disabled:opacity-50" > {isPending ? '登录中...' : '登录'} </button> </form> )}常见问题#
🤔 Q: Session 还是 JWT?
- 传统 Web 应用:Session + Cookie
- API 服务/微服务:JWT
- 混合应用:可以两者结合使用
🤔 Q: 如何安全存储密码?
使用 bcrypt 或 argon2 哈希:
import { hash, compare } from 'bcryptjs'
const hashedPassword = await hash(password, 12)const isValid = await compare(password, hashedPassword)🤔 Q: Cookie 设置最佳实践?
cookieStore.set('session', value, { httpOnly: true, // 防止 XSS secure: true, // 仅 HTTPS sameSite: 'lax', // 防止 CSRF maxAge: 60 * 60 * 24 * 7, path: '/',})下一篇将介绍 NextAuth 集成,学习如何快速实现多种登录方式。
-EOF-