乐闻世界logo
搜索文章和话题

Next.js 应用的安全最佳实践是什么?

2月17日 23:18

Next.js 应用的安全性是生产环境中不可忽视的重要方面。全面的安全防护措施可以保护应用免受各种网络攻击和数据泄露。

核心安全概念

1. 安全威胁类型

  • XSS(跨站脚本攻击):恶意脚本注入
  • CSRF(跨站请求伪造):伪造用户请求
  • SQL 注入:恶意 SQL 代码执行
  • SSRF(服务端请求伪造):伪造服务端请求
  • 点击劫持:UI 伪装攻击
  • 中间人攻击:通信拦截

安全配置和防护

1. 环境变量管理

javascript
// .env.local(开发环境) DATABASE_URL=postgresql://localhost:5432/myapp NEXTAUTH_SECRET=dev-secret-key-change-in-production NEXTAUTH_URL=http://localhost:3000 API_KEY=dev-api-key // .env.production(生产环境) DATABASE_URL=postgresql://prod-db-server:5432/myapp NEXTAUTH_SECRET=${NEXTAUTH_SECRET} # 从部署平台获取 NEXTAUTH_URL=https://myapp.com API_KEY=${API_KEY} # 从部署平台获取 // .env.example(模板文件) DATABASE_URL=postgresql://localhost:5432/myapp NEXTAUTH_SECRET=your-secret-key-here NEXTAUTH_URL=http://localhost:3000 API_KEY=your-api-key-here // .gitignore .env.local .env.production .env.development *.env // 环境变量验证 // lib/env.js import { z } from 'zod'; const envSchema = z.object({ DATABASE_URL: z.string().url(), NEXTAUTH_SECRET: z.string().min(32), NEXTAUTH_URL: z.string().url(), API_KEY: z.string().min(16), NODE_ENV: z.enum(['development', 'production', 'test']), }); export const env = envSchema.parse(process.env); // 使用环境变量 // lib/db.js import { env } from './env'; export const db = createConnection(env.DATABASE_URL);

2. CSP(内容安全策略)配置

javascript
// next.config.js const ContentSecurityPolicy = require('./lib/csp'); module.exports = { async headers() { return [ { source: '/(.*)', headers: [ { key: 'Content-Security-Policy', value: ContentSecurityPolicy.toString(), }, { key: 'X-Frame-Options', value: 'DENY', }, { key: 'X-Content-Type-Options', value: 'nosniff', }, { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin', }, { key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()', }, ], }, ]; }, }; // lib/csp.js const ContentSecurityPolicy = ` default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline' https://cdn.example.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: https: blob:; font-src 'self' https://fonts.gstatic.com; connect-src 'self' https://api.example.com; media-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; upgrade-insecure-requests; `; export default ContentSecurityPolicy; // 动态 CSP 策略 // lib/csp.js import { NextResponse } from 'next/server'; export function getCSPHeaders() { const nonce = Buffer.from(crypto.randomUUID()).toString('base64'); return { 'Content-Security-Policy': ` default-src 'self'; script-src 'self' 'nonce-${nonce}' https://cdn.example.com; style-src 'self' 'nonce-${nonce}' https://fonts.googleapis.com; img-src 'self' data: https: blob:; `.replace(/\s{2,}/g, ' ').trim(), 'X-Nonce': nonce, }; } // 在页面中使用 nonce // app/layout.js export default function RootLayout({ children }) { const nonce = headers().get('X-Nonce'); return ( <html lang="en"> <head> <script nonce={nonce} src="https://cdn.example.com/analytics.js" /> </head> <body>{children}</body> </html> ); }

3. XSS 防护

javascript
// 使用 React 的自动转义 // app/page.js export default function Page({ userContent }) { // React 自动转义,防止 XSS return <div>{userContent}</div>; } // 危险操作:使用 dangerouslySetInnerHTML export default function Page({ userContent }) { // 如果必须使用,需要先清理内容 const sanitizedContent = DOMPurify.sanitize(userContent); return ( <div dangerouslySetInnerHTML={{ __html: sanitizedContent }} /> ); } // 使用 DOMPurify 清理 HTML // lib/sanitize.js import DOMPurify from 'isomorphic-dompurify'; export function sanitizeHTML(html) { return DOMPurify.sanitize(html, { ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'ul', 'ol', 'li'], ALLOWED_ATTR: [], }); } export function sanitizeURL(url) { try { const parsed = new URL(url); if (!['http:', 'https:'].includes(parsed.protocol)) { return ''; } return parsed.href; } catch { return ''; } } // API 路由中的输入验证 // app/api/comments/route.js import { z } from 'zod'; import { sanitizeHTML } from '@/lib/sanitize'; const commentSchema = z.object({ content: z.string().min(1).max(1000), userId: z.string().uuid(), }); export async function POST(request) { const body = await request.json(); // 验证输入 const validatedData = commentSchema.parse(body); // 清理 HTML 内容 const sanitizedContent = sanitizeHTML(validatedData.content); // 保存到数据库 const comment = await saveComment({ ...validatedData, content: sanitizedContent, }); return Response.json(comment); }

4. CSRF 防护

javascript
// 使用 next-safe-action 防止 CSRF // app/actions.js 'use server'; import { action, makeSafeActionClient } from 'next-safe-action'; import { z } from 'zod'; const safeActionClient = makeSafeActionClient(); export const createComment = action( z.object({ content: z.string().min(1).max(1000), postId: z.string().uuid(), }), async ({ content, postId }) => { // 自动验证 CSRF token const comment = await createCommentInDB({ content, postId }); return { success: true, comment }; } ); // 自定义 CSRF 防护 // lib/csrf.js import { cookies } from 'next/headers'; const CSRF_SECRET = process.env.CSRF_SECRET; export async function generateCSRFToken() { const token = crypto.randomUUID(); const cookieStore = await cookies(); cookieStore.set('csrf_token', token, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'strict', maxAge: 60 * 60, // 1 hour }); return token; } export async function validateCSRFToken(token) { const cookieStore = await cookies(); const storedToken = cookieStore.get('csrf_token'); if (!storedToken || storedToken.value !== token) { throw new Error('Invalid CSRF token'); } return true; } // API 路由中使用 CSRF 防护 // app/api/comments/route.js import { validateCSRFToken } from '@/lib/csrf'; export async function POST(request) { const body = await request.json(); const csrfToken = request.headers.get('X-CSRF-Token'); // 验证 CSRF token await validateCSRFToken(csrfToken); // 处理请求 const comment = await createComment(body); return Response.json(comment); } // 客户端发送请求时包含 CSRF token // components/CommentForm.js 'use client'; import { useState, useEffect } from 'react'; export default function CommentForm({ postId }) { const [content, setContent] = useState(''); const [csrfToken, setCsrfToken] = useState(''); useEffect(() => { // 获取 CSRF token fetch('/api/csrf-token') .then(res => res.json()) .then(data => setCsrfToken(data.token)); }, []); const handleSubmit = async (e) => { e.preventDefault(); await fetch('/api/comments', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken, }, body: JSON.stringify({ content, postId }), }); }; return ( <form onSubmit={handleSubmit}> <textarea value={content} onChange={(e) => setContent(e.target.value)} /> <button type="submit">Submit</button> </form> ); }

5. 认证和授权

javascript
// 使用 NextAuth.js 进行认证 // app/api/auth/[...nextauth]/route.js import NextAuth from 'next-auth'; import CredentialsProvider from 'next-auth/providers/credentials'; import GoogleProvider from 'next-auth/providers/google'; import { verifyPassword } from '@/lib/auth'; export const { handlers, auth, signIn, signOut } = NextAuth({ providers: [ GoogleProvider({ clientId: process.env.GOOGLE_CLIENT_ID, clientSecret: process.env.GOOGLE_CLIENT_SECRET, }), CredentialsProvider({ name: 'Credentials', credentials: { email: { label: 'Email', type: 'email' }, password: { label: 'Password', type: 'password' }, }, async authorize(credentials) { const user = await getUserByEmail(credentials.email); if (!user) { throw new Error('User not found'); } const isValidPassword = await verifyPassword( credentials.password, user.password ); if (!isValidPassword) { throw new Error('Invalid password'); } return { id: user.id, email: user.email, name: user.name, role: user.role, }; }, }), ], callbacks: { async jwt({ token, user }) { if (user) { token.id = user.id; token.role = user.role; } return token; }, async session({ session, token }) { session.user.id = token.id; session.user.role = token.role; return session; }, }, pages: { signIn: '/login', error: '/auth/error', }, session: { strategy: 'jwt', maxAge: 30 * 24 * 60 * 60, // 30 days }, secret: process.env.NEXTAUTH_SECRET, }); export { handlers as GET, handlers as POST }; // 基于角色的访问控制(RBAC) // lib/auth.js import { auth } from '@/app/api/auth/[...nextauth]/config'; export const ROLES = { ADMIN: 'admin', USER: 'user', GUEST: 'guest', }; export async function requireAuth() { const session = await auth(); if (!session) { throw new Error('Unauthorized'); } return session; } export async function requireRole(role) { const session = await requireAuth(); if (session.user.role !== role) { throw new Error('Forbidden'); } return session; } export async function requireAnyRole(...roles) { const session = await requireAuth(); if (!roles.includes(session.user.role)) { throw new Error('Forbidden'); } return session; } // 在 API 路由中使用授权 // app/api/admin/users/route.js import { requireRole, ROLES } from '@/lib/auth'; export async function GET() { const session = await requireRole(ROLES.ADMIN); const users = await getAllUsers(); return Response.json(users); } // 在页面中使用授权 // app/admin/page.js import { redirect } from 'next/navigation'; import { requireRole, ROLES } from '@/lib/auth'; export default async function AdminPage() { const session = await requireRole(ROLES.ADMIN); return ( <div> <h1>Admin Dashboard</h1> <p>Welcome, {session.user.name}</p> </div> ); }

6. 数据库安全

javascript
// 使用参数化查询防止 SQL 注入 // lib/db.js import { Pool } from 'pg'; const pool = new Pool({ connectionString: process.env.DATABASE_URL, }); export async function getUserById(id) { const result = await pool.query( 'SELECT * FROM users WHERE id = $1', [id] ); return result.rows[0]; } export async function createUser(data) { const result = await pool.query( 'INSERT INTO users (email, password, name) VALUES ($1, $2, $3) RETURNING *', [data.email, data.password, data.name] ); return result.rows[0]; } // 使用 ORM 防止 SQL 注入 // lib/prisma.js import { PrismaClient } from '@prisma/client'; const prisma = new PrismaClient(); export async function getUserById(id) { return prisma.user.findUnique({ where: { id }, }); } export async function createUser(data) { return prisma.user.create({ data: { email: data.email, password: data.password, name: data.name, }, }); } // 数据库连接安全 // lib/db.js import { Pool } from 'pg'; const pool = new Pool({ connectionString: process.env.DATABASE_URL, ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false, } : false, max: 20, // 最大连接数 idleTimeoutMillis: 30000, connectionTimeoutMillis: 2000, }); // 数据库迁移安全 // prisma/migrate.ts import { PrismaClient } from '@prisma/client'; const prisma = new PrismaClient(); async function main() { // 使用事务确保数据一致性 await prisma.$transaction(async (tx) => { await tx.user.create({ data: { email: 'admin@example.com', password: await hashPassword('admin123'), role: 'admin', }, }); }); } main() .catch((e) => { console.error(e); process.exit(1); }) .finally(async () => { await prisma.$disconnect(); });

7. API 安全

javascript
// 速率限制 // lib/rate-limit.js import { Ratelimit } from '@upstash/ratelimit'; import { Redis } from '@upstash/redis'; const ratelimit = new Ratelimit({ redis: Redis.fromEnv(), limiter: Ratelimit.slidingWindow(10, '10 s'), analytics: true, }); export async function rateLimit(identifier) { const { success, limit, remaining, reset } = await ratelimit.limit(identifier); if (!success) { throw new Error('Rate limit exceeded'); } return { limit, remaining, reset }; } // 在 API 路由中使用速率限制 // app/api/comments/route.js import { rateLimit } from '@/lib/rate-limit'; import { headers } from 'next/headers'; export async function POST(request) { const ip = headers().get('x-forwarded-for') || 'unknown'; // 应用速率限制 await rateLimit(ip); const body = await request.json(); const comment = await createComment(body); return Response.json(comment); } // API 密钥验证 // lib/api-key.js export async function validateApiKey(apiKey) { const validKeys = process.env.API_KEYS?.split(',') || []; if (!validKeys.includes(apiKey)) { throw new Error('Invalid API key'); } return true; } // 在 API 路由中验证 API 密钥 // app/api/data/route.js import { validateApiKey } from '@/lib/api-key'; import { headers } from 'next/headers'; export async function GET(request) { const apiKey = headers().get('x-api-key'); // 验证 API 密钥 await validateApiKey(apiKey); const data = await fetchData(); return Response.json(data); } // 输入验证 // lib/validation.js import { z } from 'zod'; export const commentSchema = z.object({ content: z.string().min(1).max(1000), postId: z.string().uuid(), userId: z.string().uuid(), }); export const userSchema = z.object({ email: z.string().email(), password: z.string().min(8).regex(/[A-Z]/).regex(/[0-9]/), name: z.string().min(2).max(100), }); // 在 API 路由中使用验证 // app/api/users/route.js import { userSchema } from '@/lib/validation'; export async function POST(request) { const body = await request.json(); // 验证输入 const validatedData = userSchema.parse(body); // 处理请求 const user = await createUser(validatedData); return Response.json(user); }

8. 文件上传安全

javascript
// 文件上传验证 // lib/file-upload.js import { z } from 'zod'; const ALLOWED_MIME_TYPES = [ 'image/jpeg', 'image/png', 'image/gif', 'image/webp', ]; const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB export const fileSchema = z.object({ name: z.string().max(255), type: z.enum(ALLOWED_MIME_TYPES), size: z.number().max(MAX_FILE_SIZE), }); export async function validateFile(file) { // 验证文件类型 if (!ALLOWED_MIME_TYPES.includes(file.type)) { throw new Error('Invalid file type'); } // 验证文件大小 if (file.size > MAX_FILE_SIZE) { throw new Error('File too large'); } // 验证文件内容 const buffer = await file.arrayBuffer(); const uint8Array = new Uint8Array(buffer); // 检查文件头(magic numbers) if (file.type === 'image/jpeg') { if (uint8Array[0] !== 0xFF || uint8Array[1] !== 0xD8) { throw new Error('Invalid JPEG file'); } } return true; } // 安全的文件上传 API // app/api/upload/route.js import { validateFile } from '@/lib/file-upload'; import { uploadToS3 } from '@/lib/s3'; export async function POST(request) { const formData = await request.formData(); const file = formData.get('file'); if (!file) { return Response.json({ error: 'No file provided' }, { status: 400 }); } try { // 验证文件 await validateFile(file); // 生成安全的文件名 const ext = file.name.split('.').pop(); const safeName = `${crypto.randomUUID()}.${ext}`; // 上传到 S3 const url = await uploadToS3(file, safeName); return Response.json({ url }); } catch (error) { return Response.json({ error: error.message }, { status: 400 }); } }

9. 日志和监控

javascript
// 安全日志记录 // lib/logger.js import pino from 'pino'; const logger = pino({ level: process.env.LOG_LEVEL || 'info', transport: process.env.NODE_ENV === 'development' ? { target: 'pino-pretty', options: { colorize: true, }, } : undefined, }); export function logSecurityEvent(event, data) { logger.warn({ type: 'SECURITY_EVENT', event, timestamp: new Date().toISOString(), ...data, }); } export function logError(error, context = {}) { logger.error({ type: 'ERROR', error: error.message, stack: error.stack, timestamp: new Date().toISOString(), ...context, }); } // 使用示例 // app/api/auth/login/route.js import { logSecurityEvent } from '@/lib/logger'; export async function POST(request) { const body = await request.json(); try { const user = await authenticateUser(body.email, body.password); return Response.json({ user }); } catch (error) { // 记录安全事件 logSecurityEvent('LOGIN_FAILED', { email: body.email, ip: request.headers.get('x-forwarded-for'), userAgent: request.headers.get('user-agent'), }); return Response.json({ error: 'Invalid credentials' }, { status: 401 }); } }

安全最佳实践

  1. 环境变量安全: 使用 .env.local,不要提交到版本控制
  2. CSP 配置: 严格的内容安全策略
  3. 输入验证: 所有用户输入都必须验证
  4. 输出编码: 防止 XSS 攻击
  5. CSRF 防护: 使用 token 验证
  6. 认证授权: 实施适当的认证和授权机制
  7. 速率限制: 防止暴力攻击
  8. HTTPS: 强制使用 HTTPS
  9. 依赖更新: 定期更新依赖包
  10. 安全审计: 定期进行安全审计和渗透测试

通过实施这些安全措施,可以显著提高 Next.js 应用的安全性。

标签:Next.js