Appearance
认证与权限
前面几篇的接口任何人都能调——没有"你是谁"的概念。真实项目需要认证(知道请求是谁发的)和权限(这个用户能不能做这件事)。FastAPI 生态里最常见的方案是 JWT Token。
一、认证的基本流程
密码不能明文存数据库,Token 不用每次传密码——这两件事决定了认证的基本流程:
两步:登录时拿 Token,后续请求带着 Token。前端 06 篇封装的 request 函数已经做了"从 localStorage 取 Token、放进 Authorization 头"——后端要做的就是验证这个 Token。
二、密码哈希
用户密码绝对不能明文存数据库——数据库泄露等于所有密码泄露。存的是密码的哈希值(单向不可逆):
bash
uv add passlib[bcrypt]python
from passlib.context import CryptContext
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def hash_password(password: str) -> str:
return pwd_context.hash(password)
def verify_password(plain: str, hashed: str) -> bool:
return pwd_context.verify(plain, hashed)
# 注册时存哈希
stored_hash = hash_password("mypassword123")
# 登录时验证
print(verify_password("mypassword123", stored_hash)) # True
print(verify_password("wrongpassword", stored_hash)) # Falsehash_password 把明文密码转成一段完全看不出原文的字符串(哈希值),verify_password 把用户输入的明文跟存储的哈希比对。bcrypt 算法自带盐值,同一个密码每次 hash 出来的结果都不一样,但 verify 能正确比对——这是它比简单 MD5/SHA 强的地方。
三、JWT Token
JWT(JSON Web Token)是一段编码过的字符串,里面包含用户信息和签名。后端签发 Token,前端带着它发请求,后端验证签名通过后从 Token 里取出用户信息。
bash
uv add python-jose[cryptography]python
from jose import jwt, JWTError
from datetime import datetime, timedelta, timezone
SECRET_KEY = "your-secret-key-change-in-production" # 生产环境必须换一个足够随机的
ALGORITHM = "HS256"
TOKEN_EXPIRE_HOURS = 24
def create_token(data: dict) -> str:
"""生成 JWT Token。data 里放要编码的用户信息。"""
payload = data.copy()
expire = datetime.now(timezone.utc) + timedelta(hours=TOKEN_EXPIRE_HOURS)
payload.update({"exp": expire}) # 过期时间
return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)
def decode_token(token: str) -> dict:
"""解析 Token,返回里面的数据。签名不对或过期会抛异常。"""
return jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
# 签发
token = create_token({"sub": "user_001", "name": "张三"})
print(token) # eyJhbGciOi...
# 解析
payload = decode_token(token)
print(payload) # {'sub': 'user_001', 'name': '张三', 'exp': ...}create_token 把用户信息(用户 ID、用户名)编码进 Token,decode_token 把它解出来。Token 里不要放敏感信息——JWT 的编码只是 base64,不是加密,任何人都能解码看到内容。签名只保证"这个 Token 没被篡改",不保证"内容是加密的"。
SECRET_KEY 是签名的密钥——只有知道这个密钥才能签发合法 Token。生产环境必须用足够随机的长字符串(openssl rand -hex 32 生成一个),泄露了等于谁都能伪造 Token。
四、登录接口
把密码哈希和 JWT 串起来:
python
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
app = FastAPI()
# 模拟用户数据库(后面换成真实数据库)
users_db = {
"zhangsan": {
"id": 1,
"username": "zhangsan",
"hashed_password": hash_password("password123"),
}
}
class LoginRequest(BaseModel):
username: str
password: str
@app.post("/login")
def login(req: LoginRequest):
user = users_db.get(req.username)
if not user or not verify_password(req.password, user["hashed_password"]):
raise HTTPException(status_code=401, detail="用户名或密码错误")
token = create_token({"sub": str(user["id"]), "username": user["username"]})
return {"access_token": token, "token_type": "bearer"}前端拿到 access_token 后存进 localStorage,后续请求放在 Authorization: Bearer <token> 头里——跟前端 06 篇的封装完全对应。
HTTPException(status_code=401) 是 FastAPI 抛 HTTP 错误的方式——返回 401 状态码和错误详情。用户名不存在或密码不对,都返回同样的 401,不告诉具体是用户名错还是密码错,防止攻击者用错误信息猜测用户名。
五、保护接口——验证 Token
需要一个机制:请求带了合法 Token 才能访问,否则返回 401。FastAPI 用依赖注入实现:
python
from fastapi import Depends
from fastapi.security import OAuth2PasswordBearer
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login")
async def get_current_user(token: str = Depends(oauth2_scheme)):
"""从 Token 解析当前用户。Token 无效时抛 401。"""
try:
payload = decode_token(token)
user_id = payload.get("sub")
if user_id is None:
raise HTTPException(status_code=401, detail="无效的 Token")
except JWTError:
raise HTTPException(status_code=401, detail="无效的 Token")
# 根据 user_id 查用户(这里简化,直接返回 payload)
return {"id": int(user_id), "username": payload.get("username")}在需要登录的接口上加 Depends(get_current_user):
python
@app.get("/me")
def get_profile(current_user: dict = Depends(get_current_user)):
return current_user
@app.get("/articles")
def list_articles(current_user: dict = Depends(get_current_user)):
return [{"id": 1, "title": "Python 入门"}]OAuth2PasswordBearer(tokenUrl="login") 自动从请求头 Authorization: Bearer <token> 里提取 Token 字符串,传给 get_current_user。没带 Token 或 Token 无效,get_current_user 抛 401,接口不会执行。
/me 和 /articles 加了 Depends(get_current_user),变成需要登录才能访问的接口。不加这个依赖的接口(比如 /login)仍然可以匿名访问——FastAPI 的权限控制是逐接口声明的,不是全局一刀切。