Chapter 05 · 登录与权限:守门人与通行证
📚 学完本章,你将能够:
- 解释"密码不能直接存明文"的原因,并用 bcrypt 哈希密码
- 理解 JWT 的工作原理:为什么服务端"无状态"也能识别用户
- 实现完整的登录/注册流程:注册→哈希存密码→登录→发令牌→访问受保护接口
- 用 RBAC 模型控制"谁能做什么"(普通用户 vs 管理员)
- 在 Swagger UI 中测试带认证的接口
💡 场景引入
你的图书 API 现在任何人都能删除书籍。一个恶作剧用户写了个脚本,把你数据库里的书全删光了。
你需要两扇安全门:认证(Authentication)——确认"你是谁";授权(Authorization)——确认"你能做什么"。本章教你搭建这两扇门。
5.1 密码:永远不要存明文
假设你的数据库被黑客拖走了。如果密码是明文存储的,所有用户的密码直接暴露。更可怕的是,很多人在不同网站用相同密码——一个网站泄露,其他网站也危险了。
哈希(Hash)是密码存储的标准方案。它不是"加密"(因为加密可逆),而是"单向变换"——你可以把密码变成乱码,但无法从乱码反推出原密码。
from passlib.context import CryptContext
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
# 注册时:把明文密码变成哈希
hashed = pwd_context.hash("my_password")
# 结果类似:$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/X4.VTtYA.qGZvKG6
# 登录时:验证用户输入的密码是否匹配哈希
is_valid = pwd_context.verify("my_password", hashed) # True
is_valid = pwd_context.verify("wrong_password", hashed) # False
md5 和 sha1 已经过时,现代计算机几秒就能破解。一定要用 bcrypt、argon2 或 scrypt。另外,永远不要自己发明哈希算法。
5.2 JWT:服务端的"无状态"通行证
传统方案:用户登录后,服务端在内存或 Redis 里存一份"session 表",每次请求都查表确认你是谁。这在单机上简单,但多服务器部署时就麻烦了——A 服务器不知道 B 服务器创建的 session。
JWT(JSON Web Token)的思路不同:服务器不存任何状态,所有信息都写在令牌里。令牌由服务器签名,无法伪造。
用户登录
提交用户名 + 密码
服务端验证
密码哈希比对通过
生成 JWT
把用户 ID、角色、过期时间写入 JSON,用密钥签名
后续请求
客户端在 Header 中携带 Authorization: Bearer <token>
服务端验签
用密钥验证签名,解析出用户 ID,不需要查数据库
from datetime import datetime, timedelta, timezone
from jose import jwt
SECRET_KEY = "change-this-in-production" # 生产环境从环境变量读取
ALGORITHM = "HS256"
def create_access_token(user_id: str, expires_minutes: int = 30) -> str:
now = datetime.now(timezone.utc)
payload = {
"sub": user_id, # subject:主题,这里放用户 ID
"exp": now + timedelta(minutes=expires_minutes), # 过期时间
"iat": now, # 签发时间
}
return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)
def verify_token(token: str) -> dict | None:
try:
return jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
except jwt.JWTError:
return None # 令牌无效或过期
JWT 的内容只是 Base64 编码,任何人都能读取。不要在 JWT 中存放密码、身份证号等敏感信息。JWT 的价值在于签名不可伪造,而非内容保密。
5.3 完整登录流程
from fastapi.security import OAuth2PasswordBearer
# 声明 token 获取地址,FastAPI 会自动在 Swagger 中生成登录按钮
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/token")
async def get_current_user(
token: Annotated[str, Depends(oauth2_scheme)],
db: Annotated[AsyncSession, Depends(get_db)]
) -> User:
payload = verify_token(token)
if not payload:
raise HTTPException(401, "Invalid token")
user = await db.get(User, int(payload["sub"]))
if not user:
raise HTTPException(401, "User not found")
return user
from fastapi.security import OAuth2PasswordRequestForm
@app.post("/auth/token")
async def login(
form: Annotated[OAuth2PasswordRequestForm, Depends()],
db: Annotated[AsyncSession, Depends(get_db)]
):
# OAuth2PasswordRequestForm 自动解析 username + password
user = await get_user_by_username(db, form.username)
if not user or not pwd_context.verify(form.password, user.hashed_password):
raise HTTPException(401, "Incorrect username or password")
token = create_access_token(str(user.id))
return {"access_token": token, "token_type": "bearer"}
@app.get("/me")
async def read_current_user(user: Annotated[User, Depends(get_current_user)]):
return {"id": user.id, "username": user.username}
5.4 RBAC:角色权限控制
认证解决"你是谁",授权解决"你能做什么"。RBAC(Role-Based Access Control)是最简单的授权模型:给用户分配角色,角色拥有权限。
async def require_admin(user: Annotated[User, Depends(get_current_user)]) -> User:
if user.role != "admin":
raise HTTPException(403, "Admin access required")
return user
@app.delete("/books/{book_id}")
async def delete_book(
book_id: int,
admin: Annotated[User, Depends(require_admin)], # 只有管理员能删除
db: Annotated[AsyncSession, Depends(get_db)]
):
...
401 Unauthorized:"我不知道你是谁,请登录"。403 Forbidden:"我知道你是谁,但你没权限做这个操作。"很多初学者会混淆这两个状态码。
🎯 知识检测
本章小结
密码必须哈希存储
bcrypt 带随机盐,计算慢,有效抵抗暴力破解。
JWT 是无状态通行证
服务器不存 session,所有信息在令牌里。适合分布式部署。
OAuth2PasswordBearer 集成 Swagger
FastAPI 自动生成登录按钮,直接在文档界面测试受保护接口。
RBAC 控制操作权限
通过依赖链组合认证和授权,401 和 403 分工明确。