Skip to content

认证基础

🙋 用户登录后如何保持状态?如何保护需要登录才能访问的页面?

认证方式对比#

方式优点缺点适用场景
Session安全、可撤销需要存储、难扩展传统应用
JWT无状态、易扩展难撤销、体积大API、微服务
Cookie自动发送、HttpOnly大小限制与 Session/JWT 配合
app/api/login/route.ts
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 } })
}
app/api/me/route.ts
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 })
}
app/api/logout/route.ts
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#

lib/session.ts
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')
}

登录流程#

app/actions/auth.ts
'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#

lib/jwt.ts
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 刷新#

app/api/auth/refresh/route.ts
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 })
}

路由保护#

中间件保护#

middleware.ts
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).*)'],
}

服务端组件保护#

app/dashboard/page.tsx
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>
)
}

封装认证检查#

lib/auth.ts
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
}

使用:

app/admin/page.tsx
import { requireAdmin } from '@/lib/auth'
export default async function AdminPage() {
const admin = await requireAdmin()
return <div>管理员页面,{admin.name}</div>
}

客户端认证状态#

Context Provider#

context/auth.tsx
'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#

components/Header.tsx
'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>
)
}

登录表单#

components/LoginForm.tsx
'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?

🤔 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-