如何保护 API Routes 和 Server Actions?(身份验证、授权)

15 阅读5分钟

如何保护 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 基础会话管理
  • 中间件全局保护

授权

  • 基于角色的访问控制
  • 资源级权限检查
  • 细粒度权限管理

安全措施

  • 速率限制
  • 输入验证
  • 错误处理
  • 安全头部配置

最佳实践

  • 使用环境变量保护敏感信息
  • 实现适当的错误处理
  • 监控和日志记录
  • 定期安全审计