Skip to content

Next.js 安全最佳实践 🔒

🎯 概述

安全是 Web 应用的基础。Next.js 提供了多种内置安全功能和最佳实践。

📦 环境变量

安全存储

bash
# .env.local (不要提交到 Git)
DATABASE_URL="postgresql://..."
JWT_SECRET="your-secret-key"
API_KEY="your-api-key"

# .env (可以提交)
NEXT_PUBLIC_API_URL="https://api.example.com"

使用环境变量

tsx
// 服务端
const secret = process.env.JWT_SECRET

// 客户端 (必须以 NEXT_PUBLIC_ 开头)
const apiUrl = process.env.NEXT_PUBLIC_API_URL

🚀 CSRF 保护

使用 CSRF Token

tsx
// lib/csrf.ts
import { randomBytes } from 'crypto'

export function generateToken() {
  return randomBytes(32).toString('hex')
}

export function verifyToken(token: string, storedToken: string) {
  return token === storedToken
}

// app/api/form/route.ts
import { cookies } from 'next/headers'
import { verifyToken } from '@/lib/csrf'

export async function POST(request: Request) {
  const token = request.headers.get('X-CSRF-Token')
  const storedToken = cookies().get('csrf-token')?.value
  
  if (!token || !verifyToken(token, storedToken)) {
    return Response.json({ error: 'Invalid CSRF token' }, { status: 403 })
  }
  
  // 处理请求
}

📝 XSS 防护

避免 dangerouslySetInnerHTML

tsx
// ❌ 不安全
<div dangerouslySetInnerHTML={{ __html: userInput }} />

// ✅ 安全
<div>{userInput}</div>

// ✅ 如果必须使用,先清理
import DOMPurify from 'isomorphic-dompurify'

<div dangerouslySetInnerHTML={{ 
  __html: DOMPurify.sanitize(userInput) 
}} />

Content Security Policy

tsx
// middleware.ts
import { NextResponse } from 'next/server'

export function middleware(request: Request) {
  const response = NextResponse.next()
  
  response.headers.set(
    'Content-Security-Policy',
    "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';"
  )
  
  return response
}

🎨 SQL 注入防护

使用参数化查询

tsx
// ❌ 不安全
const query = `SELECT * FROM users WHERE email = '${email}'`

// ✅ 安全 (Prisma)
const user = await prisma.user.findUnique({
  where: { email }
})

// ✅ 安全 (原生 SQL)
const user = await db.query(
  'SELECT * FROM users WHERE email = $1',
  [email]
)

📊 认证和授权

JWT 验证

tsx
// lib/auth.ts
import jwt from 'jsonwebtoken'

export function verifyToken(token: string) {
  try {
    return jwt.verify(token, process.env.JWT_SECRET!)
  } catch {
    return null
  }
}

// middleware.ts
export function middleware(request: Request) {
  const token = request.headers.get('Authorization')?.split(' ')[1]
  
  if (!token || !verifyToken(token)) {
    return Response.json({ error: 'Unauthorized' }, { status: 401 })
  }
  
  return NextResponse.next()
}

权限检查

tsx
// lib/permissions.ts
export function canEditPost(userId: string, post: Post) {
  return post.authorId === userId
}

// app/api/posts/[id]/route.ts
export async function PUT(request: Request, { params }) {
  const user = await getCurrentUser(request)
  const post = await getPost(params.id)
  
  if (!canEditPost(user.id, post)) {
    return Response.json({ error: 'Forbidden' }, { status: 403 })
  }
  
  // 更新文章
}

📚 最佳实践

1. 速率限制

tsx
// lib/rate-limit.ts
import { LRUCache } from 'lru-cache'

const rateLimit = new LRUCache({
  max: 500,
  ttl: 60000, // 1分钟
})

export function checkRateLimit(ip: string) {
  const count = (rateLimit.get(ip) as number) || 0
  
  if (count > 10) {
    return false
  }
  
  rateLimit.set(ip, count + 1)
  return true
}

2. 输入验证

tsx
import { z } from 'zod'

const schema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
})

export async function POST(request: Request) {
  const body = await request.json()
  
  try {
    const data = schema.parse(body)
    // 处理有效数据
  } catch (error) {
    return Response.json({ error: 'Invalid input' }, { status: 400 })
  }
}

3. 安全头部

tsx
// next.config.js
module.exports = {
  async headers() {
    return [
      {
        source: '/:path*',
        headers: [
          {
            key: 'X-Frame-Options',
            value: 'DENY',
          },
          {
            key: 'X-Content-Type-Options',
            value: 'nosniff',
          },
          {
            key: 'Referrer-Policy',
            value: 'strict-origin-when-cross-origin',
          },
        ],
      },
    ]
  },
}

4. HTTPS

tsx
// middleware.ts
export function middleware(request: Request) {
  if (
    process.env.NODE_ENV === 'production' &&
    request.headers.get('x-forwarded-proto') !== 'https'
  ) {
    return NextResponse.redirect(
      `https://${request.headers.get('host')}${request.nextUrl.pathname}`,
      301
    )
  }
}

🔗 相关资源


下一步:学习 Next.js 构建和部署