如何保护 API Routes 和 Server Actions?(身份验证、授权)
API Routes 保护
1. 基础身份验证
// lib/auth.js
import { NextRequest } from 'next/server'
import jwt from 'jsonwebtoken'
export async function verifyToken(request) {
try {
const token = request.headers.get('authorization')?.replace('Bearer ', '')
if (!token) {
throw new Error('No token provided')
}
const decoded = jwt.verify(token, process.env.JWT_SECRET)
return decoded
} catch (error) {
throw new Error('Invalid token')
}
}
export async function requireAuth(request) {
try {
const user = await verifyToken(request)
return user
} catch (error) {
throw new Error('Authentication required')
}
}
// app/api/protected/route.js
import { NextRequest, NextResponse } from 'next/server'
import { requireAuth } from '@/lib/auth'
export async function GET(request) {
try {
// 身份验证
const user = await requireAuth(request)
// 业务逻辑
const data = await fetchUserData(user.id)
return NextResponse.json({ data })
} catch (error) {
return NextResponse.json({ error: error.message }, { status: 401 })
}
}
export async function POST(request) {
try {
const user = await requireAuth(request)
const body = await request.json()
// 创建数据
const result = await createData(body, user.id)
return NextResponse.json(result, { status: 201 })
} catch (error) {
return NextResponse.json(
{ error: error.message },
{ status: error.message.includes('Authentication') ? 401 : 400 }
)
}
}
2. 基于角色的授权
// lib/auth.js
export async function requireRole(request, requiredRole) {
const user = await requireAuth(request)
if (user.role !== requiredRole && user.role !== 'admin') {
throw new Error('Insufficient permissions')
}
return user
}
export async function requireAnyRole(request, roles) {
const user = await requireAuth(request)
if (!roles.includes(user.role) && user.role !== 'admin') {
throw new Error('Insufficient permissions')
}
return user
}
// app/api/admin/users/route.js
import { NextRequest, NextResponse } from 'next/server'
import { requireRole } from '@/lib/auth'
export async function GET(request) {
try {
// 要求管理员权限
const user = await requireRole(request, 'admin')
const users = await db.users.findMany({
select: {
id: true,
name: true,
email: true,
role: true,
createdAt: true,
},
})
return NextResponse.json({ users })
} catch (error) {
return NextResponse.json(
{ error: error.message },
{ status: error.message.includes('permissions') ? 403 : 401 }
)
}
}
export async function DELETE(request) {
try {
const user = await requireRole(request, 'admin')
const { searchParams } = new URL(request.url)
const userId = searchParams.get('id')
if (!userId) {
return NextResponse.json(
{ error: 'User ID is required' },
{ status: 400 }
)
}
await db.users.delete({
where: { id: parseInt(userId) },
})
return NextResponse.json({ message: 'User deleted successfully' })
} catch (error) {
return NextResponse.json(
{ error: error.message },
{ status: error.message.includes('permissions') ? 403 : 401 }
)
}
}
3. 资源级权限控制
// lib/auth.js
export async function requireResourceAccess(request, resourceId, resourceType) {
const user = await requireAuth(request)
// 检查用户是否有权限访问特定资源
const hasAccess = await checkResourceAccess(user.id, resourceId, resourceType)
if (!hasAccess) {
throw new Error('Access denied to this resource')
}
return user
}
async function checkResourceAccess(userId, resourceId, resourceType) {
switch (resourceType) {
case 'post':
const post = await db.posts.findUnique({
where: { id: resourceId },
select: { authorId: true, isPublic: true },
})
return post && (post.authorId === userId || post.isPublic)
case 'order':
const order = await db.orders.findUnique({
where: { id: resourceId },
select: { userId: true },
})
return order && order.userId === userId
default:
return false
}
}
// app/api/posts/[id]/route.js
import { NextRequest, NextResponse } from 'next/server'
import { requireResourceAccess } from '@/lib/auth'
export async function GET(request, { params }) {
try {
const { id } = params
const user = await requireResourceAccess(request, id, 'post')
const post = await db.posts.findUnique({
where: { id: parseInt(id) },
include: {
author: {
select: { name: true, avatar: true },
},
comments: true,
},
})
return NextResponse.json({ post })
} catch (error) {
return NextResponse.json(
{ error: error.message },
{ status: error.message.includes('Access denied') ? 403 : 401 }
)
}
}
export async function PUT(request, { params }) {
try {
const { id } = params
const user = await requireResourceAccess(request, id, 'post')
const body = await request.json()
const post = await db.posts.update({
where: { id: parseInt(id) },
data: body,
})
return NextResponse.json({ post })
} catch (error) {
return NextResponse.json(
{ error: error.message },
{ status: error.message.includes('Access denied') ? 403 : 401 }
)
}
}
Server Actions 保护
1. 基础身份验证
// lib/auth.js
export async function getServerSession() {
try {
const { cookies } = await import('next/headers')
const token = cookies().get('auth-token')?.value
if (!token) {
return null
}
const decoded = jwt.verify(token, process.env.JWT_SECRET)
return decoded
} catch (error) {
return null
}
}
export async function requireServerAuth() {
const session = await getServerSession()
if (!session) {
throw new Error('Authentication required')
}
return session
}
// app/actions/user-actions.js
'use server'
import { requireServerAuth } from '@/lib/auth'
import { db } from '@/lib/database'
import { revalidatePath } from 'next/cache'
export async function updateProfile(formData) {
try {
// 身份验证
const user = await requireServerAuth()
const name = formData.get('name')
const email = formData.get('email')
if (!name || !email) {
throw new Error('Name and email are required')
}
// 更新用户资料
const updatedUser = await db.users.update({
where: { id: user.id },
data: { name, email },
})
// 重新验证缓存
revalidatePath('/profile')
return { success: true, user: updatedUser }
} catch (error) {
return { success: false, error: error.message }
}
}
export async function deleteAccount() {
try {
const user = await requireServerAuth()
// 删除用户账户
await db.users.delete({
where: { id: user.id },
})
// 清除会话
const { cookies } = await import('next/headers')
cookies().delete('auth-token')
return { success: true }
} catch (error) {
return { success: false, error: error.message }
}
}
2. 基于角色的 Server Actions
// app/actions/admin-actions.js
'use server'
import { requireServerAuth } from '@/lib/auth'
import { db } from '@/lib/database'
export async function createUser(formData) {
try {
const user = await requireServerAuth()
// 检查管理员权限
if (user.role !== 'admin') {
throw new Error('Insufficient permissions')
}
const name = formData.get('name')
const email = formData.get('email')
const role = formData.get('role')
const newUser = await db.users.create({
data: { name, email, role },
})
return { success: true, user: newUser }
} catch (error) {
return { success: false, error: error.message }
}
}
export async function deleteUser(userId) {
try {
const user = await requireServerAuth()
if (user.role !== 'admin') {
throw new Error('Insufficient permissions')
}
await db.users.delete({
where: { id: parseInt(userId) },
})
return { success: true }
} catch (error) {
return { success: false, error: error.message }
}
}
3. 资源级权限控制
// app/actions/post-actions.js
'use server'
import { requireServerAuth } from '@/lib/auth'
import { db } from '@/lib/database'
export async function updatePost(postId, formData) {
try {
const user = await requireServerAuth()
// 检查用户是否有权限编辑这个帖子
const post = await db.posts.findUnique({
where: { id: parseInt(postId) },
select: { authorId: true },
})
if (!post || post.authorId !== user.id) {
throw new Error('Access denied')
}
const title = formData.get('title')
const content = formData.get('content')
const updatedPost = await db.posts.update({
where: { id: parseInt(postId) },
data: { title, content },
})
return { success: true, post: updatedPost }
} catch (error) {
return { success: false, error: error.message }
}
}
export async function deletePost(postId) {
try {
const user = await requireServerAuth()
const post = await db.posts.findUnique({
where: { id: parseInt(postId) },
select: { authorId: true },
})
if (!post || post.authorId !== user.id) {
throw new Error('Access denied')
}
await db.posts.delete({
where: { id: parseInt(postId) },
})
return { success: true }
} catch (error) {
return { success: false, error: error.message }
}
}
中间件保护
1. 全局身份验证中间件
// middleware.js
import { NextResponse } from 'next/server'
import jwt from 'jsonwebtoken'
export function middleware(request) {
const token = request.cookies.get('auth-token')?.value
// 保护 API 路由
if (request.nextUrl.pathname.startsWith('/api/protected')) {
if (!token) {
return NextResponse.json(
{ error: 'Authentication required' },
{ status: 401 }
)
}
try {
jwt.verify(token, process.env.JWT_SECRET)
} catch (error) {
return NextResponse.json({ error: 'Invalid token' }, { status: 401 })
}
}
// 保护管理页面
if (request.nextUrl.pathname.startsWith('/admin')) {
if (!token) {
return NextResponse.redirect(new URL('/login', request.url))
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET)
if (decoded.role !== 'admin') {
return NextResponse.redirect(new URL('/unauthorized', request.url))
}
} catch (error) {
return NextResponse.redirect(new URL('/login', request.url))
}
}
return NextResponse.next()
}
export const config = {
matcher: ['/api/protected/:path*', '/admin/:path*'],
}
2. 速率限制中间件
// lib/rate-limit.js
import { NextResponse } from 'next/server'
const rateLimitMap = new Map()
export function rateLimit(limit = 10, windowMs = 60000) {
return (request) => {
const ip = request.ip || 'unknown'
const now = Date.now()
const windowStart = now - windowMs
// 清理过期的记录
for (const [key, value] of rateLimitMap.entries()) {
if (value.timestamp < windowStart) {
rateLimitMap.delete(key)
}
}
// 检查当前 IP 的请求次数
const current = rateLimitMap.get(ip)
if (!current) {
rateLimitMap.set(ip, { count: 1, timestamp: now })
return null
}
if (current.count >= limit) {
return NextResponse.json({ error: 'Too many requests' }, { status: 429 })
}
current.count++
return null
}
}
// app/api/auth/login/route.js
import { NextRequest, NextResponse } from 'next/server'
import { rateLimit } from '@/lib/rate-limit'
export async function POST(request) {
// 应用速率限制
const rateLimitResponse = rateLimit(5, 60000)(request)
if (rateLimitResponse) {
return rateLimitResponse
}
try {
const { email, password } = await request.json()
// 验证用户凭据
const user = await authenticateUser(email, password)
if (!user) {
return NextResponse.json(
{ error: 'Invalid credentials' },
{ status: 401 }
)
}
// 生成 JWT token
const token = jwt.sign(
{ id: user.id, email: user.email, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: '24h' }
)
const response = NextResponse.json({ user })
response.cookies.set('auth-token', token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 86400,
})
return response
} catch (error) {
return NextResponse.json({ error: 'Login failed' }, { status: 500 })
}
}
最佳实践总结
1. 安全配置
// next.config.js
module.exports = {
async headers() {
return [
{
source: '/api/:path*',
headers: [
{
key: 'X-Content-Type-Options',
value: 'nosniff',
},
{
key: 'X-Frame-Options',
value: 'DENY',
},
{
key: 'X-XSS-Protection',
value: '1; mode=block',
},
],
},
]
},
}
2. 环境变量保护
// .env.local
JWT_SECRET=your-super-secret-jwt-key
DATABASE_URL=your-database-connection-string
API_SECRET_KEY=your-api-secret-key
// 验证必需的环境变量
const requiredEnvVars = ['JWT_SECRET', 'DATABASE_URL']
for (const envVar of requiredEnvVars) {
if (!process.env[envVar]) {
throw new Error(`Missing required environment variable: ${envVar}`)
}
}
3. 错误处理
// lib/error-handler.js
export function handleApiError(error) {
console.error('API Error:', error)
if (error.message.includes('Authentication')) {
return { status: 401, message: 'Authentication required' }
}
if (error.message.includes('permissions')) {
return { status: 403, message: 'Insufficient permissions' }
}
if (error.message.includes('validation')) {
return { status: 400, message: 'Validation failed' }
}
return { status: 500, message: 'Internal server error' }
}
总结
API Routes 和 Server Actions 保护策略:
身份验证:
- JWT token 验证
- Cookie 基础会话管理
- 中间件全局保护
授权:
- 基于角色的访问控制
- 资源级权限检查
- 细粒度权限管理
安全措施:
- 速率限制
- 输入验证
- 错误处理
- 安全头部配置
最佳实践:
- 使用环境变量保护敏感信息
- 实现适当的错误处理
- 监控和日志记录
- 定期安全审计