当你登录一个网站后,刷新页面不会要求重新登录,换个页面也还是登录状态。服务器明明是无状态的,它怎么知道「刚才那个人又来了」?
这就是 Web 认证体系要解决的核心问题。本文带你从 HTTP 无状态的本质出发,逐步理解 Cookie、Session、JWT 三种主流认证方案的原理、差异和最佳实践。
1. 根源:HTTP 的无状态性
HTTP 协议本身是无状态的——每个请求都是独立的,服务器不会记住「这个请求是刚才那个人发的」。
请求1: GET /api/profile
↓ 服务器:你是谁?我不知道。
请求2: GET /api/orders
↓ 服务器:你还是谁?我还是不知道。但 Web 应用需要状态:登录、购物车、浏览历史……于是,Cookie 诞生了。
2. Cookie:在浏览器里「贴标签」
2.1 工作原理
Cookie 是服务器让浏览器帮忙存储的一小段数据(通常 4KB 以内)。浏览器在每次请求时自动携带。
┌────────┐ ┌────────┐
│ 浏览器 │ ─── GET /api/profile ────→ │ 服务器 │
│ │ ←── 200 + Set-Cookie: │ │
│ │ user_id=abc123 ────── │ │
│ │ │ │
│ 浏览器 │ ─── GET /api/orders ─────→ │ 服务器 │
│ (自动 │ Cookie: user_id=abc123 │ (读取 │
│ 带上) │ ←── 200 [订单数据] ────── │ Cookie)│
└────────┘ └────────┘2.2 Set-Cookie 属性
Set-Cookie: session_id=abc123; HttpOnly; Secure; SameSite=Lax; Max-Age=3600; Path=/| 属性 | 说明 | 安全影响 |
|---|---|---|
HttpOnly |
禁止 JavaScript 访问 | 防止 XSS 窃取 Cookie |
Secure |
仅 HTTPS 传输 | 防止中间人拦截 |
SameSite |
跨站请求是否携带 | 防止 CSRF 攻击 |
Max-Age |
过期时间(秒) | 持久化 vs 会话 |
Domain |
Cookie 生效域名 | 控制作用范围 |
Path |
Cookie 生效路径 | 精细控制 |
2.3 在服务端设置 Cookie
Node.js (Express):
// 登录成功,设置 Cookie
res.cookie('user_id', 'abc123', {
httpOnly: true,
secure: true, // 生产环境使用 HTTPS
sameSite: 'strict',
maxAge: 24 * 60 * 60 * 1000, // 24 小时
});
// 读取 Cookie
app.get('/api/profile', (req, res) => {
const userId = req.cookies.user_id;
// 根据 userId 查询用户数据...
});Python (Flask):
from flask import Flask, request, make_response
@app.route('/login', methods=['POST'])
def login():
# 验证用户名密码...
resp = make_response({'status': 'ok'})
resp.set_cookie(
'user_id', 'abc123',
httponly=True,
secure=True,
samesite='Strict',
max_age=86400
)
return resp
@app.route('/api/profile')
def profile():
user_id = request.cookies.get('user_id')
# 查询用户信息...3. Session:把数据存在服务端
3.1 为什么 Cookie 不够用?
- Cookie 大小限制(4KB),存不下复杂数据
- Cookie 存储在客户端,可以被篡改
- 敏感信息不应放在客户端
因此,Session 机制诞生:Cookie 只存一个 Session ID,真正的数据存在服务端。
3.2 工作流程
┌────────┐ ┌────────┐ ┌──────────┐
│ 浏览器 │ │ 服务器 │ │ 存储层 │
│ │ │ │ │(Redis/DB)│
└───┬────┘ └───┬────┘ └────┬─────┘
│ │ │
│ POST /login │ │
│ {user, password} │ │
│────────────────────────────→│ │
│ │ 验证密码 │
│ │ 生成 session │
│ │──────────────→│ set session:xyz
│ │ │ {user_id, role}
│ ← 200 │ │
│ Set-Cookie: │ │
│ session_id=xyz │ │
│ │ │
│ GET /api/profile │ │
│ Cookie: session_id=xyz │ │
│────────────────────────────→│ │
│ │ 查询 session │
│ │──────────────→│ get session:xyz
│ │←──────────────│ {user_id, role}
│ ← 200 【用户数据】 │ │3.3 Node.js 实现(express-session + Redis)
const express = require('express');
const session = require('express-session');
const RedisStore = require('connect-redis').default;
const { createClient } = require('redis');
const app = express();
const redisClient = createClient({ url: 'redis://localhost:6379' });
redisClient.connect();
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: 'your-secret-key-change-in-production',
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 24 * 60 * 60 * 1000, // 24 小时
},
}));
// 登录接口
app.post('/login', (req, res) => {
const { username, password } = req.body;
// 验证逻辑...
if (isValidUser(username, password)) {
req.session.user = { id: 123, username, role: 'admin' };
res.json({ success: true });
} else {
res.status(401).json({ error: '用户名或密码错误' });
}
});
// 受保护的接口
app.get('/api/profile', (req, res) => {
if (!req.session.user) {
return res.status(401).json({ error: '请先登录' });
}
res.json({ user: req.session.user });
});
// 登出
app.post('/logout', (req, res) => {
req.session.destroy(err => {
res.json({ success: true });
});
});3.4 Python 实现(Flask-Session + Redis)
from flask import Flask, session
from flask_session import Session
import redis
app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-secret-key'
app.config['SESSION_TYPE'] = 'redis'
app.config['SESSION_REDIS'] = redis.from_url('redis://localhost:6379')
app.config['SESSION_PERMANENT'] = True
app.config['PERMANENT_SESSION_LIFETIME'] = 86400 # 24 小时
Session(app)
@app.route('/login', methods=['POST'])
def login():
data = request.get_json()
# 验证用户名密码...
if is_valid_user(data['username'], data['password']):
session['user'] = {
'id': 123,
'username': data['username'],
'role': 'admin'
}
return jsonify(success=True)
return jsonify(error='认证失败'), 401
@app.route('/api/profile')
def profile():
user = session.get('user')
if not user:
return jsonify(error='请先登录'), 401
return jsonify(user=user)
@app.route('/logout', methods=['POST'])
def logout():
session.clear()
return jsonify(success=True)4. JWT:把「身份证」交给用户
4.1 JWT 是什么?
JWT(JSON Web Token)是一种自包含的令牌,用户数据直接编码在 Token 中,服务端不需要存储任何会话信息。
eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjEyMywicm9sZSI6ImFkbWluIn0.abc123def456
│ Header │ Payload │ Signature │
│ Base64 编码的 │ Base64 编码的用户数据 │ 防篡改签名 │
│ 算法信息 │ │ │4.2 工作流程
┌────────┐ ┌────────┐
│ 客户端 │ │ 服务器 │
│ │ POST /login │ │
│ │ {user, password} │ │
│ │─────────────────────────────→│ │
│ │ │ 验证 │
│ │ ← 200 { token: "eyJ..." } │ 签发 JWT│
│ │ │ │
│ 客户端 │ │ │
│ 存储 │ GET /api/profile │ │
│ JWT 到 │ Authorization: Bearer eyJ..│ │
│localStor│─────────────────────────────→│ 校验签名│
│ age │ │ 读取 payload│
│ │ ← 200 【用户数据】 │ │
└────────┘ └────────┘4.3 Node.js 实现
const jwt = require('jsonwebtoken');
const express = require('express');
const app = express();
const SECRET_KEY = 'your-secret-key-change-me';
// 签发 JWT
app.post('/login', (req, res) => {
const { username, password } = req.body;
if (isValidUser(username, password)) {
const token = jwt.sign(
{ userId: 123, username, role: 'admin' }, // payload
SECRET_KEY,
{ expiresIn: '24h' } // 过期时间
);
res.json({ token });
} else {
res.status(401).json({ error: '认证失败' });
}
});
// JWT 验证中间件
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // "Bearer TOKEN"
if (!token) {
return res.status(401).json({ error: '未提供 Token' });
}
jwt.verify(token, SECRET_KEY, (err, user) => {
if (err) {
return res.status(403).json({ error: 'Token 无效或已过期' });
}
req.user = user;
next();
});
}
// 受保护路由
app.get('/api/profile', authenticateToken, (req, res) => {
res.json({ user: req.user });
});4.4 Python 实现(PyJWT)
import jwt
from datetime import datetime, timedelta
from functools import wraps
from flask import Flask, request, jsonify
app = Flask(__name__)
SECRET_KEY = 'your-secret-key-change-me'
# 签发 JWT
@app.route('/login', methods=['POST'])
def login():
data = request.get_json()
if is_valid_user(data['username'], data['password']):
token = jwt.encode(
{
'userId': 123,
'username': data['username'],
'role': 'admin',
'exp': datetime.utcnow() + timedelta(hours=24),
},
SECRET_KEY,
algorithm='HS256'
)
return jsonify(token=token)
return jsonify(error='认证失败'), 401
# JWT 验证装饰器
def require_auth(f):
@wraps(f)
def decorated(*args, **kwargs):
auth_header = request.headers.get('Authorization', '')
if not auth_header.startswith('Bearer '):
return jsonify(error='未提供 Token'), 401
token = auth_header.split(' ')[1]
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])
request.current_user = payload
except jwt.ExpiredSignatureError:
return jsonify(error='Token 已过期'), 401
except jwt.InvalidTokenError:
return jsonify(error='无效的 Token'), 401
return f(*args, **kwargs)
return decorated
@app.route('/api/profile')
@require_auth
def profile():
return jsonify(user=request.current_user)5. 三种方案全面对比
| 维度 | Cookie(明文) | Session | JWT |
|---|---|---|---|
| 存储位置 | 客户端 | 服务端 | 客户端 |
| 状态 | 有状态 | 有状态 | 无状态 |
| 扩展性 | 差(需共享存储) | 差(需共享存储) | 好(天然支持) |
| 安全性 | 低(可篡改) | 高(服务端可信) | 高(签名防篡改) |
| 性能 | 高 | 中(每次查存储) | 高(本地校验) |
| 注销控制 | 即时 | 即时 | 困难(需黑名单) |
| 移动端友好 | 一般 | 一般 | 好(Header 传递) |
| 跨域 | 受同源策略限制 | 受同源策略限制 | 天然支持 |
| 数据大小 | 极有限(4KB) | 不限制 | 有限(Header 限制) |
| 适用场景 | 简单状态标记 | 传统 Web 应用 | API / 微服务 / SPA |
6. 安全注意事项
6.1 XSS(跨站脚本攻击)
攻击者注入恶意脚本 → 盗取 Cookie / localStorage 中的 Token防御:
// ✅ 对 Session Cookie 设置 HttpOnly(JS 无法读取)
res.cookie('session_id', value, { httpOnly: true });
// ⚠️ JWT 存在 localStorage 中可以被 XSS 读取
// 缓解:将 JWT 也存为 HttpOnly Cookie,或用 refresh token 机制6.2 CSRF(跨站请求伪造)
受害者访问恶意网站 → 恶意网站自动提交请求 → 浏览器自动带上 Cookie防御:
# Flask 示例:CSRF Token
from flask_wtf.csrf import CSRFProtect
csrf = CSRFProtect(app)
# 前端请求时带上 CSRF Token
# 或使用 SameSite Cookie 属性
@app.after_request
def set_cookie(response):
response.set_cookie('session_id', value, samesite='Strict')
return response6.3 JWT 特殊注意事项
// ❌ 不要把敏感信息放在 payload 中
// payload 只是 Base64 编码,不是加密!
jwt.sign({
userId: 123,
password: 'secret123' // ❌ 任何人都能解码看到!
}, SECRET_KEY);
// ✅ 只放非敏感的标识信息
jwt.sign({
userId: 123,
role: 'user'
}, SECRET_KEY);
// ✅ 使用较短的过期时间
jwt.sign(payload, SECRET_KEY, { expiresIn: '15m' }); // access token
// ✅ 配合 refresh token 实现长期登录
// access token: 15 分钟过期
// refresh token: 7 天过期,存在 HttpOnly Cookie 中6.4 安全最佳实践清单
| 措施 | Cookie/Session | JWT |
|---|---|---|
| 使用 HTTPS | ✅ 必须 | ✅ 必须 |
| HttpOnly Cookie | ✅ 推荐 | ✅ 推荐(存 token 时) |
| Secure Cookie | ✅ 必须 | ✅ 必须 |
| SameSite Cookie | ✅ Strict/Lax | ✅ Strict/Lax |
| 限制过期时间 | ✅ | ✅ Access Token 短(15min) |
| 服务端登出黑名单 | N/A | 可选 |
| 密码哈希存储 | ✅ bcrypt | ✅ bcrypt |
| 速率限制 | ✅ 登录接口 | ✅ 登录接口 |
7. 选型指南
你的应用架构是什么样的?
├── 传统服务端渲染 (SSR)
│ └── 用 Session + Cookie(成熟、安全)
│
├── SPA + REST API(同一域名)
│ └── 两者皆可
│ 简单场景 → Session + Cookie
│ 需要扩展 → JWT + HttpOnly Cookie
│
├── SPA + API(跨域)
│ └── JWT(Authorization Header)
│
├── 微服务架构
│ └── JWT(无状态,服务间无需查询)
│
├── 移动端 App
│ └── JWT(不依赖 Cookie)
│
└── 需要第三方集成 / OAuth2
└── JWT(标准格式,广泛支持)8. 小结
三种方案各有优劣,没有银弹,关键看场景:
- Cookie:最简单,但功能有限,适合少量非敏感数据
- Session:传统 Web 应用首选,安全可控,但需要服务端存储
- JWT:现代 API / 微服务首选,无状态、易扩展,但要处理好过期和注销
无论选哪种方案,安全三件套不能忘:HTTPS + HttpOnly + SameSite。
认证是 Web 安全的基石,花时间把这一块吃透,受益终身。