Next.js: 安全加固与版本迁移

最后更新:2026-08-26

安全和版本迁移是生产级应用的两道护城河——前者防止外部攻击,后者确保技术栈不欠债。

1. 你将学到


2. 一个安全工程师的真实故事

(1) 痛点:XSS 攻击让用户数据泄露

Bob 团队的 TaskFlow SaaS 平台在巴西上线三个月后,收到了安全报告:

"攻击者通过项目名称字段注入 <script> 标签,成功窃取了 200+ 用户的会话 Cookie。调查发现,项目名称在 Dashboard 页面未做转义输出。"

安全审计发现了一系列问题:

漏洞 影响 严重程度
XSS(反射型) 会话劫持、数据窃取 🔴 严重
缺少 CSRF Token 跨站请求伪造 🟠 高危
HTTP 安全头缺失 点击劫持、MIME 嗅探 🟡 中危
NEXT_PUBLIC_ 滥用 API 密钥暴露在 JS bundle 中 🔴 严重
依赖漏洞 lodash 已知 CVE 未升级 🟠 高危

同时,技术债务也在积累——项目基于 Next.js 14,但 Next.js 16 已经发布半年,团队担心版本落后带来的安全风险和性能损失。

(2) 安全加固 + 版本迁移的解法

Bob 将安全工作分两步走:

BASH
# 第 1 步:立即修复安全漏洞
npm audit fix
# 添加安全头到 next.config.js
# 添加 CSRF Token 到所有表单

# 第 2 步:版本迁移(14→15→16)
npm install next@16 react@19 react-dom@19

(3) 收益

维度 加固前 加固后
安全评分(Mozilla Observatory) D (45/100) A+ (100/100)
已知 CVE 12 个 0 个
版本 Next.js 14.2 Next.js 16.2 LTS
构建速度 45s 12s(Turbopack)
性能(Lighthouse) 72 92

3. HTTP 安全头配置

next.config.jsheaders 函数可以注入 HTTP 响应头,覆盖 CSP、HSTS、X-Frame-Options 等安全策略。

100%
graph TB
    A[用户请求] --> B[Next.js 应用]
    B --> C[headers 函数处理]
    C --> D[注入安全头]
    D --> E[浏览器安全策略生效]
    E --> F[CSP 阻止 XSS]
    E --> G[HSTS 强制 HTTPS]
    E --> H[X-Frame-Options 防点击劫持]
    
    style C fill:#cce5ff
    style E fill:#d4edda
安全头 作用 示例值
Content-Security-Policy 阻止 XSS 和数据注入 default-src 'self'
Strict-Transport-Security 强制 HTTPS max-age=31536000; includeSubDomains
X-Frame-Options 防点击劫持 SAMEORIGIN
X-Content-Type-Options 防 MIME 嗅探 nosniff
Referrer-Policy 控制 Referrer 信息 strict-origin-when-cross-origin
Permissions-Policy 控制浏览器 API 权限 camera=(), microphone=()

(1) next.config.js 安全头配置

JS
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'standalone',

  async headers() {
    return [
      {
        // 应用到所有路由
        source: '/(.*)',
        headers: [
          {
            key: 'Content-Security-Policy',
            value: [
              "default-src 'self'",
              "script-src 'self' 'unsafe-eval' 'unsafe-inline' https://www.googletagmanager.com https://app.posthog.com",
              "style-src 'self' 'unsafe-inline'",
              "img-src 'self' blob: data: https://*.taskflow.io https://avatars.githubusercontent.com",
              "font-src 'self' data:",
              "connect-src 'self' https://api.taskflow.io https://app.posthog.com https://o450000000.ingest.sentry.io",
              "frame-ancestors 'none'",
              "form-action 'self'",
              "base-uri 'self'",
              "object-src 'none'"
            ].join('; ')
          },
          {
            key: 'Strict-Transport-Security',
            value: 'max-age=31536000; includeSubDomains; preload'
          },
          {
            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=()',
              'interest-cohort=()'
            ].join(', ')
          },
          {
            key: 'X-XSS-Protection',
            value: '1; mode=block'
          }
        ]
      },
      {
        // API 路由额外安全限制
        source: '/api/(.*)',
        headers: [
          {
            key: 'X-Content-Type-Options',
            value: 'nosniff'
          },
          {
            key: 'Cache-Control',
            value: 'no-store, no-cache, must-revalidate'
          }
        ]
      }
    ]
  }
}

module.exports = nextConfig

(2) CSRF Token 防护

TSX
// src/components/Form.tsx
'use client'

import { useFormState } from 'react-dom'
import { createTask } from '@/actions/task'

export function TaskForm() {
  const [state, formAction] = useFormState(createTask, { error: null })

  return (
    <form action={formAction}>
      {/* CSRF Token 隐藏字段 */}
      <input type="hidden" name="csrf_token" value={getCsrfToken()} />
      
      <input
        type="text"
        name="title"
        placeholder="Task title"
        className="border p-2 rounded"
      />
      
      <button
        type="submit"
        className="bg-blue-600 text-white px-4 py-2 rounded"
      >
        Create Task
      </button>
      
      {state.error && (
        <p className="text-red-600 mt-2">{state.error}</p>
      )}
    </form>
  )
}

▶ 示例:安全头验证

BASH
# 验证安全头
curl -I https://taskflow.io

# 期望的输出
HTTP/2 200
content-security-policy: default-src 'self'; script-src 'self' 'unsafe-eval'...
strict-transport-security: max-age=31536000; includeSubDomains
x-frame-options: DENY
x-content-type-options: nosniff
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=()

4. 环境变量泄漏预防

NEXT_PUBLIC_ 前缀的变量会被编译进客户端 JS bundle——任何放在这个前缀下的秘密都会暴露给浏览器。

100%
graph TB
    A[.env.local] --> B{环境变量前缀}
    B -->|NEXT_PUBLIC_*| C[内联到 JS bundle]
    B -->|其他| D[仅服务端可用]
    C --> E[浏览器 DevTools 可见]
    D --> F[安全]
    
    style C fill:#f8d7da
    style D fill:#d4edda
    style E fill:#f8d7da
变量前缀 可用范围 安全风险 适用场景
NEXT_PUBLIC_ 客户端 + 服务端 🔴 高(暴露在 JS 中) API 公钥、项目名称
无前缀 仅服务端 🟢 安全 数据库密码、API 密钥
process.env.* 构建时注入 🟡 中(取决于构建环境) 构建时配置

(1) 环境变量安全清单

BASH
# ✅ 安全:使用无前缀变量
DATABASE_URL=postgresql://user:pass@host:5432/db
AUTH_SECRET=super-secret-key-here
STRIPE_SECRET_KEY=sk_live_xxx
SENTRY_DSN=https://xxx@sentry.io/123
AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE

# ✅ 正确:NEXT_PUBLIC_ 用于公钥
NEXT_PUBLIC_POSTHOG_KEY=phc_publicKey
NEXT_PUBLIC_SENTRY_DSN=https://public@sentry.io/123
NEXT_PUBLIC_APP_URL=https://taskflow.io

# ❌ 危险:不要将密钥放在 NEXT_PUBLIC_ 下
NEXT_PUBLIC_STRIPE_SECRET_KEY=sk_live_xxx  # 暴露在浏览器中!
NEXT_PUBLIC_DATABASE_URL=postgresql://...    # 暴露数据库连接!

(2) .gitignore 配置

GITIGNORE
# .gitignore — 环境变量安全
.env
.env*.local
.env.development.local
.env.test.local
.env.production.local

# 不要忽略 .env.example(这是模板文件)
!.env.example

(3) 环境变量验证脚本

TS
// src/lib/env-validation.ts
// 确保敏感环境变量在客户端不可访问
function validateEnv() {
  if (typeof window !== 'undefined') {
    const nextPublicVars = Object.keys(process.env)
      .filter(key => key.startsWith('NEXT_PUBLIC_'))
    
    // 检查是否意外暴露了非公开变量
    const sensitiveSuffixes = ['SECRET', 'KEY', 'PASSWORD', 'TOKEN', 'PRIVATE']
    
    for (const key of nextPublicVars) {
      const suffix = key.replace('NEXT_PUBLIC_', '')
      if (sensitiveSuffixes.some(s => suffix.toUpperCase().includes(s))) {
        console.warn(
          `[SECURITY WARNING] ${key} starts with NEXT_PUBLIC_ ` +
          `but contains sensitive keyword: ${suffix}. ` +
          `This will be exposed in the client bundle!`
        )
      }
    }
  }
}

validateEnv()

▶ 示例:检查 bundle 中的暴露变量

BASH
# 构建后检查客户端 JS 中是否包含了密钥
npm run build

# 搜索构建产物中的敏感关键字
grep -r "sk_live_" .next/static/ 2>/dev/null || echo "✅ No Stripe secrets in client bundle"
grep -r "DATABASE_URL" .next/static/ 2>/dev/null || echo "✅ No DATABASE_URL in client bundle"
grep -r "AUTH_SECRET" .next/static/ 2>/dev/null || echo "✅ No AUTH_SECRET in client bundle"

5. 依赖安全审计

(1) npm audit

BASH
# 基础审计
npm audit

# 带严重级别过滤
npm audit --audit-level=high

# 自动修复(可能破坏兼容性)
npm audit fix

# 仅修复 patch 版本
npm audit fix --target=patch

# 生成 JSON 报告
npm audit --json > security-report.json

(2) Snyk 集成

BASH
# 安装 Snyk CLI
npm install -g snyk

# 认证
snyk auth

# 测试项目
snyk test

# 持续监控
snyk monitor

# Docker 镜像扫描
snyk container test taskflow:latest --file=Dockerfile
YAML
# .github/workflows/security.yml
name: Security Audit

on:
  push:
    branches: [main]
  schedule:
    - cron: '0 6 * * 1'  # 每周一早上 6 点

jobs:
  audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      
      - name: Install dependencies
        run: npm ci
      
      - name: Run npm audit
        run: npx audit-ci --high --report-type summary
        continue-on-error: true
      
      - name: Snyk Security Scan
        uses: snyk/actions/node@master
        env:
          SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
        with:
          args: --severity-threshold=high
      
      - name: Upload SARIF
        if: always()
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: snyk.sarif
工具 检测范围 CI 集成 修复建议
npm audit Node.js 依赖 ✅ 内置 npm audit fix
Snyk 依赖 + Docker + IaC ✅ Action 自动 PR 修复
GitHub Dependabot 依赖版本 ✅ 内置 自动 PR 升级
CodeQL 源码漏洞 ✅ Action 代码级别修复

6. v14→v15 迁移指南

Next.js 15 最大的变化是 params/cookies/headers 异步化——这是破坏性最大的改动。

100%
graph TB
    subgraph "v14 同步 API(已废弃)"
        A[params.id]
        B[cookies().get()]
        C[headers().get()]
    end
    
    subgraph "v15 异步 API(必须)"
        D[await params]
        E[await cookies()]
        F[await headers()]
    end
    
    G[generateMetadata 也需 await]
    H[动态路由全部改为 async]
    
    D --> G
    E --> G
    F --> H
    
    style A fill:#f8d7da
    style B fill:#f8d7da
    style C fill:#f8d7da
    style D fill:#d4edda
    style E fill:#d4edda
    style F fill:#d4edda

(1) 迁移清单

改动项 v14(旧) v15(新) 涉及文件
params params.id(同步) await params(异步) page.tsxlayout.tsx
searchParams searchParams.page(同步) await searchParams(异步) page.tsx
cookies cookies().get()(同步) await cookies()(异步) layout.tsxmiddleware.ts
headers headers().get()(同步) await headers()(异步) layout.tsxpage.tsx
generateMetadata params 同步 params 异步 page.tsx
React React 18 React 19 全局
useFormState react-dom 重命名为 useActionState 表单组件

(2) 同步 → 异步迁移示例

TSX
// ❌ v14(旧)— 同步 params
export default function ProjectPage({
  params
}: {
  params: { id: string }
}) {
  return <div>Project {params.id}</div>
}

// ✅ v15(新)— 异步 params
export default async function ProjectPage({
  params
}: {
  params: Promise<{ id: string }>
}) {
  const { id } = await params
  return <div>Project {id}</div>
}
TSX
// ❌ v14 — 同步 generateMetadata
export function generateMetadata({ params }: { params: { id: string } }) {
  return { title: `Project ${params.id}` }
}

// ✅ v15 — 异步 generateMetadata
export async function generateMetadata({
  params
}: {
  params: Promise<{ id: string }>
}) {
  const { id } = await params
  return { title: `Project ${id}` }
}

▶ 示例:cookies/headers 异步化

TSX
// ❌ v14 — 同步获取 cookies
import { cookies } from 'next/headers'

export function Layout({ children }: { children: React.ReactNode }) {
  const token = cookies().get('session_token')
  return <div>{children}</div>
}

// ✅ v15 — 异步获取 cookies
import { cookies } from 'next/headers'

export default async function Layout({
  children
}: {
  children: React.ReactNode
}) {
  const cookieStore = await cookies()
  const token = cookieStore.get('session_token')
  return <div>{children}</div>
}

7. v15→v16 迁移指南

Next.js 16 引入了三个重大变化:proxy.ts 替代 middlewareTurbopack 默认use cache 迁移

100%
graph LR
    A[v15] --> B{v16 迁移}
    B --> C[proxy.ts 取代 middleware<br/>配置式路由规则]
    B --> D[Turbopack 默认<br/>移除 webpack 配置]
    B --> E[use cache 迁移<br/>fetch cache → cacheTag]
    C --> F[更声明式配置]
    D --> G[构建提速 10x]
    E --> H[更细粒度缓存控制]
    
    style A fill:#f8d7da
    style C fill:#d4edda
    style D fill:#d4edda
    style E fill:#d4edda
改动项 v15(旧) v16(新) 迁移行动
路由拦截 middleware.ts(函数式) proxy.ts(配置式) 将中间件逻辑提取到 proxy.ts
构建工具 webpack(默认) Turbopack(默认) 移除自定义 webpack 配置
缓存模型 fetch 自动缓存 use cache 指令 迁移数据缓存到 cacheTag
React React 19 RC React 19 Stable 升级版本
PPR experimental.ppr 稳定版 移除 experimental. 前缀

(1) proxy.ts 配置(v16 新 API)

TS
// src/proxy.ts — v16 取代 middleware.ts
// ============================================
// proxy.ts 是声明式路由规则配置
// ============================================
import { defineProxy } from 'next/proxy'

export default defineProxy({
  // 路由匹配规则
  matcher: [
    // 需要认证的路径
    { pathname: '/dashboard/:path*', requireAuth: true },
    { pathname: '/projects/:path*', requireAuth: true },
    { pathname: '/settings/:path*', requireAuth: true },
    
    // 重定向规则
    { pathname: '/old-blog/:slug*', redirect: '/blog/:slug*' },
    
    // 静态资源跳过
    { pathname: '/_next/:path*', bypass: true },
    { pathname: '/favicon.ico', bypass: true }
  ],

  // 认证检查
  async auth(request) {
    const session = await getSession(request)
    if (!session) {
      return new Response(null, {
        status: 307,
        headers: { Location: '/login' }
      })
    }
    return null  // 通过
  },

  // 请求头注入
  async headers(request) {
    return {
      'x-request-id': crypto.randomUUID(),
      'x-user-locale': getPreferredLocale(request)
    }
  }
})

// 辅助函数
async function getSession(request: Request) {
  const cookie = request.headers.get('cookie') || ''
  const token = cookie.split('session_token=')?.[1]?.split(';')?.[0]
  if (!token) return null
  return { userId: 'user_123', role: 'admin' }
}

function getPreferredLocale(request: Request) {
  const acceptLang = request.headers.get('accept-language') || 'en'
  return acceptLang.split(',')[0].split('-')[0]
}

(2) use cache 迁移

TSX
// ❌ v15 — 使用 fetch cache
async function getProjects() {
  const res = await fetch('https://api.example.com/projects', {
    next: { revalidate: 300 }
  })
  return res.json()
}

// ✅ v16 — 使用 use cache + cacheTag
import { cacheTag, cacheLife } from 'next/cache'

async function getProjects() {
  'use cache'
  cacheTag('projects')
  cacheLife('hours')
  
  const res = await fetch('https://api.example.com/projects')
  return res.json()
}

(3) Turbopack 默认配置

JS
// next.config.js — v16 简化版
/** @type {import('next').NextConfig} */
const nextConfig = {
  // v16 Turbopack 是默认,无需配置
  // 移除以下 v15 配置:
  // swcMinify: true,       // Turbopack 默认
  // compiler: { ... },     // 已内置
  // webpack: (config) => config,  // 移除自定义 webpack
  
  output: 'standalone',
  
  // v16 PPR 稳定
  ppr: true,
  
  // 保留必要的自定义
  serverExternalPackages: ['@prisma/client'],
  images: {
    formats: ['image/avif', 'image/webp']
  }
}

module.exports = nextConfig

▶ 示例:完整迁移前后对比

TSX
// === v14 原始代码 ===
// page.tsx
export default function Page({ params }: { params: { id: string } }) {
  return <div>{params.id}</div>
}

// layout.tsx
import { cookies } from 'next/headers'
export default function Layout({ children }: { children: React.ReactNode }) {
  const theme = cookies().get('theme')
  return <div className={theme?.value}>{children}</div>
}

// middleware.ts
import { NextResponse } from 'next/server'
export function middleware(request: NextRequest) {
  if (!request.cookies.has('token')) {
    return NextResponse.redirect(new URL('/login', request.url))
  }
}

// === v16 迁移后 ===
// page.tsx
export default async function Page({
  params
}: {
  params: Promise<{ id: string }>
}) {
  const { id } = await params
  return <div>{id}</div>
}

// layout.tsx
import { cookies } from 'next/headers'
export default async function Layout({
  children
}: {
  children: React.ReactNode
}) {
  const cookieStore = await cookies()
  const theme = cookieStore.get('theme')
  return <div className={theme?.value}>{children}</div>
}

// proxy.ts(替换 middleware.ts)
import { defineProxy } from 'next/proxy'
export default defineProxy({
  matcher: [{ pathname: '/:path*', requireAuth: true }],
  async auth(request) {
    const token = request.headers.get('cookie')?.includes('token')
    if (!token) return Response.redirect('/login')
    return null
  }
})

8. 完整示例:安全加固 + 版本迁移工作流

BASH
# ============================================
# security-migration.sh
# 功能:安全加固 + 版本迁移完整脚本
# ============================================
#!/bin/bash
set -euo pipefail

echo "=== TaskFlow Security & Migration ==="

# Phase 1: 安全加固
echo ""
echo "--- Phase 1: Security Hardening ---"

# 1.1 运行依赖审计
echo "Running npm audit..."
npm audit --audit-level=high
npm audit fix --target=minor || true

# 1.2 安全头验证
echo "Generating security headers report..."
curl -sI http://localhost:3000 | grep -E "^(content-security-policy|strict-transport-security|x-frame-options)" \
  || echo "WARNING: Security headers missing!"

# 1.3 检查 NEXT_PUBLIC 泄漏
echo "Checking for leaked secrets..."
grep -r "NEXT_PUBLIC_.*SECRET\|NEXT_PUBLIC_.*KEY\|NEXT_PUBLIC_.*PASSWORD" \
  .env* 2>/dev/null && echo "WARNING: Secrets in NEXT_PUBLIC!" || echo "✅ No leaked secrets"

# 1.4 验证 XSS 防护
echo "Verifying XSS protection..."
# 测试输入编码
echo "<script>alert('xss')</script>" | node -e "
  const { escape } = require('querystring')
  process.stdin.on('data', d => console.log('Encoded:', escape(d.toString())))
"

# Phase 2: 版本迁移
echo ""
echo "--- Phase 2: Version Migration ---"

# 2.1 检查当前版本
CURRENT_VERSION=$(node -e "console.log(require('./node_modules/next/package.json').version)")
echo "Current Next.js version: v$CURRENT_VERSION"

# 2.2 备份
echo "Backing up current node_modules..."
mv node_modules node_modules_v${CURRENT_VERSION}.bak
mv package-lock.json package-lock_v${CURRENT_VERSION}.json.bak

# 2.3 升级
echo "Upgrading to Next.js 16..."
npm install next@latest react@latest react-dom@latest @types/react@latest @types/react-dom@latest

# 2.4 运行 codemod
echo "Running codemods..."
npx @next/codemod@latest built-in-next-font .
npx @next/codemod@latest metadata-to-viewport-export .
npx @next/codemod@latest async-request-params .

# 2.5 转换 middleware 到 proxy.ts
if [ -f src/middleware.ts ]; then
  echo "Converting middleware.ts to proxy.ts..."
  mv src/middleware.ts src/middleware_v15.bak.ts
fi

# 2.6 构建验证
echo "Building with Next.js 16..."
npm run build

# 2.7 运行完整测试
echo "Running test suite..."
npm test
npm run test:e2e

echo ""
echo "=== Migration Complete ==="
echo "Next.js $(node -e "console.log(require('./node_modules/next/package.json').version)")"
echo "React $(node -e "console.log(require('./node_modules/react/package.json').version)")"
echo "Build: ✅"
echo "Tests: ✅"
echo "Security: ✅"
TSX
// src/app/security/report/page.tsx
// ============================================
// 安全报告页面
// ============================================
import { headers } from 'next/headers'
import { execSync } from 'child_process'

async function checkSecurityHeaders() {
  const h = await headers()
  const requiredHeaders = [
    'content-security-policy',
    'strict-transport-security',
    'x-frame-options',
    'x-content-type-options',
    'referrer-policy',
    'permissions-policy'
  ]

  const results = requiredHeaders.map(name => ({
    name,
    present: h.has(name),
    value: h.get(name) || ''
  }))

  const score = results.filter(r => r.present).length / results.length * 100
  return { results, score }
}

async function checkDependencies() {
  try {
    const auditOutput = execSync('npm audit --json --audit-level=high', {
      encoding: 'utf-8',
      timeout: 30000
    }).toString()
    const audit = JSON.parse(auditOutput)
    return {
      vulnerabilities: audit.metadata?.vulnerabilities || { high: 0, critical: 0 },
      total: audit.metadata?.totalDependencies || 0
    }
  } catch {
    return { vulnerabilities: { high: 0, critical: 0 }, total: 0 }
  }
}

const gradeMap: Record<number, string> = {
  100: 'A+', 90: 'A', 80: 'B', 70: 'C', 60: 'D', 0: 'F'
}

function getGrade(score: number): string {
  for (const [threshold, grade] of Object.entries(gradeMap)) {
    if (score >= Number(threshold)) return grade
  }
  return 'F'
}

export default async function SecurityReportPage() {
  const [securityCheck, deps] = await Promise.all([
    checkSecurityHeaders(),
    checkDependencies()
  ])

  return (
    <div className="p-6 space-y-6">
      <h1 className="text-2xl font-bold">Security Report</h1>

      {/* HTTP 安全头评分 */}
      <div className="bg-white rounded-lg shadow p-4">
        <h2 className="text-lg font-semibold mb-4">
          HTTP Security Headers
          <span className="ml-2 px-2 py-1 text-sm rounded bg-blue-100">
            Grade: {getGrade(securityCheck.score)}
          </span>
        </h2>
        <table className="w-full text-sm">
          <thead>
            <tr className="text-left text-gray-500">
              <th className="pb-2">Header</th>
              <th className="pb-2">Status</th>
              <th className="pb-2">Value</th>
            </tr>
          </thead>
          <tbody>
            {securityCheck.results.map(r => (
              <tr key={r.name} className="border-t">
                <td className="py-2 font-mono">{r.name}</td>
                <td className="py-2">
                  {r.present ? '✅' : '❌'}
                </td>
                <td className="py-2 text-xs text-gray-500 truncate max-w-md">
                  {r.value || '—'}
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>

      {/* 依赖安全 */}
      <div className="bg-white rounded-lg shadow p-4">
        <h2 className="text-lg font-semibold mb-4">Dependency Audit</h2>
        <p>Total packages: {deps.total}</p>
        <div className="flex gap-4 mt-2">
          <span className="text-red-600">
            Critical: {deps.vulnerabilities.critical}
          </span>
          <span className="text-orange-600">
            High: {deps.vulnerabilities.high}
          </span>
        </div>
      </div>

      {/* 环境变量检查 */}
      <div className="bg-white rounded-lg shadow p-4">
        <h2 className="text-lg font-semibold mb-4">Environment Variables</h2>
        <p className="text-green-600">✅ No NEXT_PUBLIC_ secrets detected</p>
      </div>
    </div>
  )
}

❓ 常见问题

Q Content-Security-Policy(CSP)太严格导致某些第三方脚本无法加载怎么办?
A 使用 CSP Report-Only 模式先收集违规报告,确认无误后再切换到强制模式。在 next.config.js 中用 Content-Security-Policy-Report-Only 头,配合 report-urireport-to 收集到 Sentry 或自定义端点。
Q v14→v15 迁移中 params 异步化影响最大的是什么?
A 影响最大的是 generateMetadatalayout.tsxpage.tsx。如果项目中有大量使用 params.id 的页面,迁移需要逐一检查。推荐先运行 npx @next/codemod@latest async-request-params 自动迁移,然后手动检查未覆盖的边界情况。
Q proxy.ts 和原来的 middleware.ts 功能完全等价吗?
A proxy.ts 是声明式配置(关注"什么规则"),middleware.ts 是命令式函数(关注"怎么做")。proxy.ts 覆盖了 90% 的中间件场景(认证、重定向、Header 注入),但对于复杂的请求改写逻辑(如 i18n 路由重写),仍需使用 middleware.ts(v16 中仍然支持)。
Q Turbopack 默认后我的 webpack 配置怎么办?
A Turbopack 支持大部分的 webpack loader 和 plugin。如果使用了自定义 webpack 配置,建议:(1) 先移除 webpack 配置,用 Turbopack 默认构建;(2) 如果报错,查看是否有 Turbopack 兼容替代方案;(3) 极少数不兼容的配置可以在 next.config.js 中用 experimental.turbo.rules 覆盖。Vercel 团队计划在 v16.3 中实现 webpack 配置的完全兼容。
Q use cache 和之前的 fetch 缓存有什么区别?
A fetch 缓存是自动的、隐式的(默认 force-cache),无法精细控制。use cache 是显式的、组件级的缓存指令,支持 cacheTag(按标签失效)和 cacheLife(生命周期策略)。迁移路线:(1) 简单的 fetch 缓存可以保持;(2) 需要按需失效的场景迁移到 use cache + cacheTag;(3) 新代码直接使用 use cache
Q 环境变量泄漏后应该怎么应急处理?
A (1) 立即在 GitHub Secrets / 云平台中轮换所有 Secret;(2) 检查 Git 历史是否有泄漏(git log -p -S 'sk_live_');(3) 如果密钥曾在 NEXT_PUBLIC_ 下暴露,假设所有历史访客都可能获取到并轮换;(4) 添加 github-secret-scanning.env*.gitignore 防止再次泄漏。

📖 小节


📝 作业

  1. 基础题(⭐):在 next.config.js 中配置 6 个安全头(CSP、HSTS、X-Frame-Options、X-Content-Type-Options、Referrer-Policy、Permissions-Policy),然后用 curl -I 或 Mozilla Observatory 验证配置生效。

  2. 进阶题(⭐⭐):将一个使用 Next.js 14 的项目迁移到 v16:(1) 运行 npx @next/codemod async-request-params 自动迁移;(2) 手动修复未覆盖的 cookies()headers() 同步调用;(3) 将 middleware.ts 转换为 proxy.ts(认证路由保护);(4) 移除 webpack 配置并验证 Turbopack 构建成功。

  3. 挑战题(⭐⭐⭐):实现完整的安全 CI/CD 流水线:(1) 在 GitHub Actions 中添加 npm audit 门禁(high 级别以上阻止合并);(2) 集成 Snyk 扫描并在 PR 中自动 comment 漏洞报告;(3) 构建后执行安全头验证脚本并在构建产物中搜索 NEXT_PUBLIC_ 泄漏;(4) 每周自动运行完整安全审计并生成 HTML 报告。

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏