3. 安全架构:验证与防御
从"邮箱验证"到"多层机器人防御",从"IP 访问"到"域名路由",理解安全的层层防线。
3.1 身份认证体系(NextAuth + Prisma)
身份认证不是单一插件,而是一个协同系统:
| 组件 | 角色 | 类比 | 存储位置 |
|---|---|---|---|
| PostgreSQL | 用户数据存储 | 保险柜 | Docker db 容器 |
| Prisma | 数据库操作层 | 会计(查账记账) | Next.js 服务端 |
| NextAuth.js | 身份校验 | 酒店前台(核对证件/发房卡) | Next.js API 层 |
NextAuth 的"邮箱验证"流程
用户输入邮箱 → NextAuth 生成加密 Token → 写入数据库 VerificationToken 表
↓
调用邮件服务(Resend)发送验证链接
↓
用户点击链接 → NextAuth 校验 Token
↓
Prisma 更新 User.emailVerified 字段
原理说明:
NextAuth 提供验证流程,但不提供"发件箱"。你需要自备 SMTP 服务(如 Resend)。验证流程:
- 用户注册,NextAuth 生成加密 Token
- 调用你配置的邮件服务发送验证链接
- 用户点击链接,NextAuth 校验 Token
- Prisma 更新数据库中
emailVerified字段
为什么自建认证而非用第三方?
| 维度 | NextAuth + Resend | Clerk / Supabase |
|---|---|---|
| 数据主权 | 用户邮箱存在自己数据库 | 存在第三方平台 |
| 费用 | 邮件量不大时几乎免费 | 按用户量收费 |
| 可控性 | 完全自控 | 依赖平台稳定性 |
| 集成度 | 完美配合 Prisma | 需要适配层 |
NextAuth + Resend 配置
# .env 中配置
RESEND_API_KEY=re_xxxxxxxxxxxx # Resend API 密钥
RESEND_FROM=onboarding@resend.dev # 开发环境发件地址
NEXTAUTH_SECRET=your-secret-here # JWT 签名密钥(用 openssl rand -base64 32 生成)
NEXTAUTH_URL=http://localhost:3000 # 认证回调地址(按阶段修改)
Resend 开发环境的限制
重要规则:开发环境(onboarding@resend.dev)只能发给自己。
原理说明:
- 反垃圾邮件机制:Resend 在验证域名前处于"沙箱(Sandbox)"模式
- 使用
resend.dev发件时,邮件只会发送到注册 Resend 时使用的邮箱- 验证域名的本质:当你添加并验证了自己的域名后,你在向全球邮局声明"我拥有这个域名",Resend 才会解除限制,允许向全球任何邮箱发信
3.2 多层防御体系
邮箱验证解决"虚假账号"问题,但无法阻止脚本疯狂调用发信接口消耗你的额度。需要在前面增加更多防线。
第一层:蜜罐技术(Honeypot)—— 对用户无感的陷阱
<!-- 在注册表单中添加一个对人类隐藏的字段 -->
<input
type="text"
name="website" <!-- 字段名像正常字段,吸引机器人填充 -->
style="display: none" <!-- CSS 隐藏:人类看不见 -->
tabindex="-1" <!-- Tab 跳过:键盘导航也看不见 -->
autoComplete="off"
/>
原理说明:
- 自动化脚本会扫描并填充所有表单字段
- 人类用户:看不见该框,提交时该字段为空
- 机器人:自动填入数据
- 后端收到该字段有值 → 直接判定攻击 → 丢弃请求不发信
- 对真实用户 100% 透明,完全无感
第二层:速率限制(Rate Limiting)—— 流量的闸机
// 后端校验逻辑伪代码
const canSend = await rateLimit.check({
ip: request.ip, // 同一 IP 1 分钟内只能请求 1 次
email: body.email, // 同一邮箱 10 分钟内只能触发 1 次
});
if (!canSend) {
return res.status(429).json({ error: "请求过于频繁,请稍后再试" });
}
原理说明:
- IP 限制:防止同一机器刷爆你的发信接口
- 邮箱限制:防止同一地址重复触发验证
- 双重限制可有效防止恶意脚本瞬间消耗你的邮件配额
第三层:人机校验(Cloudflare Turnstile)—— 智能门卫
推荐使用 Cloudflare Turnstile(Google reCAPTCHA 的现代替代品):
# 1. .env 中配置
NEXT_PUBLIC_TURNSTILE_SITE_KEY=1x00000000000000000000xx
TURNSTILE_SECRET_KEY=0x000000000000000000000000000000000000000
// 2. 前端引入 Turnstile 组件
// 大部分情况用户无需点击,Cloudflare 在后台自动验证
// 3. 后端验证 Token 真实性
const result = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
method: 'POST',
body: `secret=${secret}&response=${token}`,
});
原理说明:
- Turnstile 在后台分析浏览器指纹、鼠标轨迹等,大部分用户无需任何操作
- 验证通过后颁发 Token
- 后端拿 Token 去 Cloudflare 确认真伪
- 极难被脚本破解,且用户体验远好于"找灭火器"式图片验证
防御优先级建议
优先级 1:NextAuth + Resend 邮箱验证 → 确保账号真实性
优先级 2:蜜罐字段 → 拦截基础脚本
优先级 3:Cloudflare Turnstile → 上线前务必开启
3.3 从 IP 到域名:网络流量路径
一个项目从"本地运行"走向"公网运营",最核心的转变在于从"直接访问"升级为"代理访问"。
流量穿透的"三级跳"
用户浏览器 → ① 域名解析(DNS) → ② 云平台防火墙(安全组) → ③ Nginx 反向代理 → 内部容器
第一跳:域名解析
用户输入
openrise.com,DNS 将该域名指向你的服务器公网 IP。域名是马甲,IP 才是真实地址。
第二跳:云平台防火墙(安全组)
90% 的部署失败原因在此:腾讯云安全组没有放通 80/443 端口。即便服务器内的应用在运行,外部流量连网卡都碰不到。
第三跳:Nginx 反向代理
Nginx 监听 80 端口,识别请求域名,转发给对应的内部容器。
Nginx 反向代理配置示例
# nginx/nginx.conf
server {
listen 80;
server_name openrise.com; # 监听的域名
location / {
proxy_pass http://frontend:3000; # 转发给 Next.js 容器
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location /api/ {
proxy_pass http://backend:8000; # API 转发给后端容器
proxy_set_header Host $host;
}
# 静态资源缓存 7 天
location /_next/static/ {
proxy_pass http://frontend:3000;
expires 7d;
add_header Cache-Control "public, immutable";
}
}
原理说明:
listen 80:Nginx 监听宿主机的 80 端口proxy_pass http://frontend:3000:Nginx 将请求内部转发到 Docker 网络的 Frontend 容器- 容器间通信通过服务名(
frontend而非localhost),Docker 内置 DNS 自动解析- 为什么不让 Next.js 直接监听 80? 因为多站点场景下,一台服务器只有一个 80 端口,但可以运行多个项目,Nginx 根据
server_name(域名)区分
3.4 NEXTAUTH_URL:被忽略的关键变量
在 OAuth 和邮箱验证流程中,NEXTAUTH_URL 是不可或缺的环境变量。
为什么必须填写?
原理说明:
- OAuth 授权是一个闭环过程:第三方平台需要知道"用户授权后送回哪里"
- 容器内部只知道自己在
localhost:3000,不知道外面是域名还是 IPNEXTAUTH_URL赋予容器**"全局视角"**,告诉它对外暴露的真实地址
按阶段设置
# 本地开发阶段
NEXTAUTH_URL=http://localhost:3000
# 初期部署(无域名)
NEXTAUTH_URL=http://你的服务器公网IP
# 正式运营(有域名)
NEXTAUTH_URL=https://www.openrise.com
3.5 域名、IP 与服务器备案
物权关系
| 实体 | 性质 | 说明 |
|---|---|---|
| 服务器 | 购买的空间和算力 | 提供一个固定公网 IP |
| 域名 | 单独购买 | 不自带服务器,只是一个"指向标" |
| 备案(ICP) | 中国大陆合规要求 | 域名通过 80/443 提供服务必须备案 |
IP 访问 vs 域名
| 场景 | 推荐 | 原因 |
|---|---|---|
| 前期调试 | IP 直接访问 | 无需备案,快速验证 |
| 正式运营 | 域名 (已备案) | SSL 证书绑定域名,提供 HTTPS |
| 开发阶段 | localhost |
稳定,不受代理/VPN 影响 |
注意:
http://0.0.0.0:3000不可用(会 502);http://198.18.0.1:3000是保留地址,易出现代理转发失败导致的 502。
架构师避坑清单
- 安全组放行:部署完发现 IP 打不开?90% 原因是安全组没开 80 端口
- Docker 监听地址:容器内 Next.js 必须监听
0.0.0.0,否则 Nginx 在容器外找不到它 - 端口映射规范:Nginx 映射宿主机的
80:80;Next.js 只需内网暴露 3000,不映射宿主机
← 返回知识库 | → 下一篇:服务器环境搭建