Appearance
38|密码哈希与 JWT 认证
运维平台需要区分用户身份:谁登录了、能做什么操作。认证(Authentication)验证"你是谁",授权(Authorization)决定"你能做什么"。这篇先讲认证的基础:安全存储密码、颁发和验证 JWT Token。
一、密码哈希
绝对不要明文存储密码。即使数据库被攻破,攻击者拿到的是不可逆的哈希值,无法直接获得原始密码。
bcrypt
bash
uv add bcryptpython
import bcrypt
# 注册时:哈希密码后存储
password = "user_password"
hashed = bcrypt.hashpw(password.encode(), bcrypt.gensalt(rounds=12))
# hashed: b'$2b$12$...'
# 登录时:校验密码
is_valid = bcrypt.checkpw(password.encode(), hashed)| 参数 | 作用 |
|---|---|
bcrypt.gensalt(rounds=12) | 生成盐值,rounds 是计算强度,越大越慢越安全 |
bcrypt.hashpw() | 生成哈希 |
bcrypt.checkpw() | 校验密码是否匹配 |
bcrypt 自带盐值,每次哈希结果都不同,即使两个用户使用相同密码,存储的哈希值也不一样。
为什么不直接用 MD5/SHA256
MD5 和 SHA 系列是快速哈希,设计目的是计算校验和,不是存储密码。攻击者可以用 GPU 每秒计算数十亿次哈希,暴力破解容易。
bcrypt 是慢哈希,故意设计得计算缓慢(可调 rounds),让暴力破解成本极高。
二、JWT(JSON Web Token)
用户登录成功后,服务端颁发一个 JWT Token,客户端后续请求带上这个 Token,服务端验证 Token 的有效性来识别用户身份。
JWT 结构(三部分,用点号分隔):
header.payload.signature| 部分 | 内容 |
|---|---|
| Header | 算法类型(如 HS256) |
| Payload | 声明(claims),如用户 ID、角色、过期时间 |
| Signature | 用密钥对前两部分签名,防止篡改 |
安装
bash
uv add pyjwt生成 Token
python
import jwt
from datetime import datetime, timedelta
SECRET_KEY = "your-secret-key-change-in-production"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
def create_access_token(data: dict) -> str:
to_encode = data.copy()
expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode.update({"exp": expire})
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
# 使用
token = create_access_token({"sub": "admin", "role": "admin"})| Claim | 含义 |
|---|---|
sub | Subject,通常是用户标识 |
exp | 过期时间(Unix 时间戳) |
iat | 签发时间 |
验证 Token
python
def decode_token(token: str) -> dict:
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
return payload
except jwt.ExpiredSignatureError:
raise ValueError("Token 已过期")
except jwt.InvalidTokenError:
raise ValueError("Token 无效")三、FastAPI 集成
OAuth2PasswordBearer
FastAPI 提供 OAuth2PasswordBearer 方便地从请求头中提取 Token:
python
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/login")
async def get_current_user(token: str = Depends(oauth2_scheme)):
payload = decode_token(token)
user_id = payload.get("sub")
if user_id is None:
raise HTTPException(status_code=401, detail="无效凭证")
return user_id
@app.get("/api/me")
async def read_me(user_id: str = Depends(get_current_user)):
return {"user_id": user_id}OAuth2PasswordBearer 自动从 Authorization: Bearer <token> 请求头中提取 Token。没有 Token 或格式不对时,自动返回 401。
登录接口
python
from fastapi import Form
@app.post("/api/login")
async def login(username: str = Form(...), password: str = Form(...)):
user = get_user_by_username(username)
if not user or not bcrypt.checkpw(password.encode(), user.password_hash):
raise HTTPException(status_code=401, detail="用户名或密码错误")
token = create_access_token({"sub": user.id, "role": user.role})
return {"access_token": token, "token_type": "bearer"}四、安全要点
密钥管理
SECRET_KEY 必须足够随机,生产环境不能硬编码在代码中:
python
import os
SECRET_KEY = os.environ.get("SECRET_KEY")
if not SECRET_KEY:
raise ValueError("SECRET_KEY 环境变量未设置")Token 过期时间
Access Token 不宜过长(通常 15~60 分钟),减少 Token 泄露后的风险窗口。需要长期保持登录的,用 Refresh Token 机制:
- 登录时颁发短效的 Access Token(30 分钟)和长效的 Refresh Token(7 天)
- Access Token 过期后,用 Refresh Token 换取新的 Access Token
- Refresh Token 可以撤销(如用户修改密码后使所有 Refresh Token 失效)
HTTPS
Token 在请求头中明文传输,必须通过 HTTPS 发送,否则中间人可以截获 Token。
五、常见错误
明文存储密码
python
# 错误
user.password = request.password # 明文!
# 正确
user.password_hash = bcrypt.hashpw(request.password.encode(), bcrypt.gensalt())Token 没有设置过期时间
python
# 错误:永久有效的 Token,泄露后永远可用
jwt.encode({"sub": "admin"}, SECRET_KEY, algorithm="HS256")
# 正确:设置 exp
jwt.encode({"sub": "admin", "exp": datetime.utcnow() + timedelta(minutes=30)}, ...)在 Token 中存放敏感信息
python
# 错误:JWT 的 payload 只是 base64 编码,没有加密,任何人都能解码读取
jwt.encode({"sub": "admin", "password": "123456"}, ...)
# 正确:只放用户标识和角色
jwt.encode({"sub": "admin", "role": "admin"}, ...)用对称算法 HS256 但密钥泄露
HS256 用同一个密钥签名和验证。如果密钥泄露,攻击者可以伪造任意 Token。多服务场景考虑用 RS256(非对称算法),服务端用私钥签名,其他服务用公钥验证。