Next.js: 安全加固与版本迁移
最后更新:2026-08-26
安全和版本迁移是生产级应用的两道护城河——前者防止外部攻击,后者确保技术栈不欠债。
1. 你将学到
- 应用 XSS 防护、CSRF Token 和 HTTP 安全头(next.config.js headers)
- 防止环境变量泄漏(NEXT_PUBLIC_* 规范 / .env*.local gitignore)
- 使用 npm audit 和 Snyk 进行依赖安全审计
- 掌握 v14→v15 迁移:params/cookies/headers 异步化
- 掌握 v15→v16 迁移:proxy.ts 替代 middleware、Turbopack 默认、use cache 迁移
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 将安全工作分两步走:
# 第 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.js 的 headers 函数可以注入 HTTP 响应头,覆盖 CSP、HSTS、X-Frame-Options 等安全策略。
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 安全头配置
// 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 防护
// 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>
)
}
▶ 示例:安全头验证
# 验证安全头
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——任何放在这个前缀下的秘密都会暴露给浏览器。
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) 环境变量安全清单
# ✅ 安全:使用无前缀变量
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 — 环境变量安全
.env
.env*.local
.env.development.local
.env.test.local
.env.production.local
# 不要忽略 .env.example(这是模板文件)
!.env.example
(3) 环境变量验证脚本
// 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 中的暴露变量
# 构建后检查客户端 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
# 基础审计
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 集成
# 安装 Snyk CLI
npm install -g snyk
# 认证
snyk auth
# 测试项目
snyk test
# 持续监控
snyk monitor
# Docker 镜像扫描
snyk container test taskflow:latest --file=Dockerfile
# .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 异步化——这是破坏性最大的改动。
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.tsx、layout.tsx |
| searchParams | searchParams.page(同步) |
await searchParams(异步) |
page.tsx |
| cookies | cookies().get()(同步) |
await cookies()(异步) |
layout.tsx、middleware.ts |
| headers | headers().get()(同步) |
await headers()(异步) |
layout.tsx、page.tsx |
| generateMetadata | params 同步 |
params 异步 |
page.tsx |
| React | React 18 | React 19 | 全局 |
| useFormState | react-dom |
重命名为 useActionState |
表单组件 |
(2) 同步 → 异步迁移示例
// ❌ 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>
}
// ❌ 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 异步化
// ❌ 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 替代 middleware、Turbopack 默认、use cache 迁移。
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)
// 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 迁移
// ❌ 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 默认配置
// 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
▶ 示例:完整迁移前后对比
// === 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. 完整示例:安全加固 + 版本迁移工作流
# ============================================
# 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: ✅"
// 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>
)
}
❓ 常见问题
next.config.js 中用 Content-Security-Policy-Report-Only 头,配合 report-uri 或 report-to 收集到 Sentry 或自定义端点。params 异步化影响最大的是什么?generateMetadata、layout.tsx 和 page.tsx。如果项目中有大量使用 params.id 的页面,迁移需要逐一检查。推荐先运行 npx @next/codemod@latest async-request-params 自动迁移,然后手动检查未覆盖的边界情况。proxy.ts 和原来的 middleware.ts 功能完全等价吗?proxy.ts 是声明式配置(关注"什么规则"),middleware.ts 是命令式函数(关注"怎么做")。proxy.ts 覆盖了 90% 的中间件场景(认证、重定向、Header 注入),但对于复杂的请求改写逻辑(如 i18n 路由重写),仍需使用 middleware.ts(v16 中仍然支持)。next.config.js 中用 experimental.turbo.rules 覆盖。Vercel 团队计划在 v16.3 中实现 webpack 配置的完全兼容。use cache 和之前的 fetch 缓存有什么区别?fetch 缓存是自动的、隐式的(默认 force-cache),无法精细控制。use cache 是显式的、组件级的缓存指令,支持 cacheTag(按标签失效)和 cacheLife(生命周期策略)。迁移路线:(1) 简单的 fetch 缓存可以保持;(2) 需要按需失效的场景迁移到 use cache + cacheTag;(3) 新代码直接使用 use cache。git log -p -S 'sk_live_');(3) 如果密钥曾在 NEXT_PUBLIC_ 下暴露,假设所有历史访客都可能获取到并轮换;(4) 添加 github-secret-scanning 和 .env* 到 .gitignore 防止再次泄漏。📖 小节
- HTTP 安全头(CSP、HSTS、X-Frame-Options 等)在
next.config.js的headers函数中配置,是防御 XSS/点击劫持的第一道防线 NEXT_PUBLIC_前缀的变量会暴露在客户端 JS 中,绝不对存放密钥。.env*.local必须加入.gitignorenpm audit和 Snyk 定期扫描依赖漏洞,CI 中集成安全审计门禁- v14→v15 迁移核心:
params/cookies/headers全部改为异步,useFormState更名为useActionState - v15→v16 迁移核心:
proxy.ts声明式配置替代middleware.ts函数式中间件、Turbopack 成为默认构建工具、use cache指令替代隐式fetch缓存 - 安全加固 + 版本迁移应分阶段执行:先修复高危漏洞,再升级版本,每步都经过完整测试验证
📝 作业
-
基础题(⭐):在
next.config.js中配置 6 个安全头(CSP、HSTS、X-Frame-Options、X-Content-Type-Options、Referrer-Policy、Permissions-Policy),然后用curl -I或 Mozilla Observatory 验证配置生效。 -
进阶题(⭐⭐):将一个使用 Next.js 14 的项目迁移到 v16:(1) 运行
npx @next/codemod async-request-params自动迁移;(2) 手动修复未覆盖的cookies()和headers()同步调用;(3) 将middleware.ts转换为proxy.ts(认证路由保护);(4) 移除webpack配置并验证 Turbopack 构建成功。 -
挑战题(⭐⭐⭐):实现完整的安全 CI/CD 流水线:(1) 在 GitHub Actions 中添加
npm audit门禁(high 级别以上阻止合并);(2) 集成 Snyk 扫描并在 PR 中自动 comment 漏洞报告;(3) 构建后执行安全头验证脚本并在构建产物中搜索NEXT_PUBLIC_泄漏;(4) 每周自动运行完整安全审计并生成 HTML 报告。