系列:Python Web 框架
很多 FastAPI 教程会停在“写几个路由”和“自动文档很好用”。但真实 API 项目绕不开登录。
这篇不做完整用户中心,只写一个最小但靠谱的 JWT 鉴权流程:
注册用户密码哈希入库登录校验密码签发 access token访问受保护接口示例为了聚焦流程,用内存字典模拟数据库。真实项目把 fake_users_db 换成数据库查询即可。FastAPI 基础部分可以先看 FastAPI 安装与使用指南,依赖注入和项目结构可以接着看 FastAPI 进阶使用指南。
安装依赖
创建项目:
mkdir fastapi-auth-democd fastapi-auth-demopython3 -m venv .venvsource .venv/bin/activate安装依赖:
pip install fastapi uvicorn[standard] python-jose[cryptography] passlib[bcrypt] python-multipart几个包的作用:
fastapi Web 框架uvicorn ASGI 服务器python-jose 生成和解析 JWTpasslib[bcrypt] 密码哈希python-multipart 支持 OAuth2PasswordRequestForm 表单项目结构
先用一个小结构,不要一上来拆太细:
fastapi-auth-demo/├── main.py├── auth.py└── schemas.py后续接数据库时,再拆 models.py、database.py、repositories/。
定义请求和响应模型
schemas.py:
from pydantic import BaseModel, EmailStr
class UserCreate(BaseModel): email: EmailStr password: str
class UserPublic(BaseModel): email: str
class Token(BaseModel): access_token: str token_type: str = "bearer"如果使用 EmailStr,还需要装:
pip install pydantic[email]这里没有返回用户 id,是为了让示例更短。真实项目通常会有 id、created_at、is_active 等字段。
密码哈希
不要明文存密码。即使是个人项目,也不要。
auth.py:
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_password: str, password_hash: str) -> bool: return pwd_context.verify(plain_password, password_hash)hash_password() 用来注册时入库,verify_password() 用来登录时比对。
生成 JWT
继续写 auth.py:
from datetime import datetime, timedelta, timezonefrom jose import jwt
SECRET_KEY = "change-me-in-production"ALGORITHM = "HS256"ACCESS_TOKEN_EXPIRE_MINUTES = 30
def create_access_token(subject: str) -> str: expires_at = datetime.now(timezone.utc) + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) payload = { "sub": subject, "exp": expires_at, } return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)sub 一般放用户唯一标识,这里放邮箱。
注意:SECRET_KEY 生产环境必须从环境变量读取,不要写死。示例里写死只是方便跑通。
写注册和登录接口
main.py:
from fastapi import FastAPI, HTTPException, statusfrom fastapi.security import OAuth2PasswordRequestFormfrom fastapi import Depends
from auth import create_access_token, hash_password, verify_passwordfrom schemas import Token, UserCreate, UserPublic
app = FastAPI()
fake_users_db: dict[str, dict[str, str]] = {}
@app.post("/register", response_model=UserPublic, status_code=status.HTTP_201_CREATED)def register(user: UserCreate): if user.email in fake_users_db: raise HTTPException(status_code=400, detail="email already registered")
fake_users_db[user.email] = { "email": user.email, "password_hash": hash_password(user.password), } return {"email": user.email}
@app.post("/token", response_model=Token)def login(form: OAuth2PasswordRequestForm = Depends()): user = fake_users_db.get(form.username) if not user or not verify_password(form.password, user["password_hash"]): raise HTTPException(status_code=401, detail="invalid credentials")
token = create_access_token(subject=user["email"]) return {"access_token": token}这里有个小别扭:OAuth2PasswordRequestForm 的字段叫 username,即使你拿它当 email 用,字段名也还是 username。这是 OAuth2 规范里的历史包袱。
启动:
uvicorn main:app --reload注册:
curl -X POST http://127.0.0.1:8000/register \ -H 'Content-Type: application/json' \ -d '{"email":"alice.local@test","password":"secret123"}'登录:
curl -X POST http://127.0.0.1:8000/token \ -H 'Content-Type: application/x-www-form-urlencoded' \ -d 'username=alice.local@test&password=secret123'保护接口
现在要实现:请求头带 Authorization: Bearer <token> 才能访问 /me。
补充 auth.py:
from fastapi import Depends, HTTPException, statusfrom fastapi.security import OAuth2PasswordBearerfrom jose import JWTError, jwt
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
def get_token_subject(token: str = Depends(oauth2_scheme)) -> str: try: payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) subject = payload.get("sub") if not subject: raise HTTPException(status_code=401, detail="invalid token") return subject except JWTError as exc: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="invalid token", headers={"WWW-Authenticate": "Bearer"}, ) from excmain.py 增加:
from auth import get_token_subject
@app.get("/me", response_model=UserPublic)def read_me(email: str = Depends(get_token_subject)): user = fake_users_db.get(email) if not user: raise HTTPException(status_code=404, detail="user not found") return {"email": user["email"]}调用:
TOKEN="上一步返回的 access_token"
curl http://127.0.0.1:8000/me \ -H "Authorization: Bearer $TOKEN"到这里,一个最小的 JWT 登录闭环就通了。
接数据库时怎么改
示例里的 fake_users_db 不能用于生产。接数据库后,关键变化只有两处:
注册时:查询 email 是否存在,保存 password_hash登录时:按 email 查询用户,校验 password_hash解析 token 后:按 sub 查询用户伪代码大概是:
def get_user_by_email(session, email: str): return session.query(User).filter(User.email == email).first()
def create_user(session, email: str, password: str): user = User(email=email, password_hash=hash_password(password)) session.add(user) session.commit() return user如果你用 SQLModel,可以参考 FastAPI 进阶使用指南 里的数据库分层方式,不要把 SQL 写进路由函数里。
常见问题
JWT 要不要存数据库
普通 access token 不需要存数据库。服务端只要能验证签名和过期时间即可。
但如果你要支持“主动退出登录后立即失效”,就需要引入黑名单、token version 或更短的 access token + refresh token 机制。
access token 过期时间设多久
内部系统可以 30 分钟到 2 小时。公开产品通常更短,再配 refresh token。
不要把 access token 设成几个月。泄露之后风险太大。
JWT 放 localStorage 还是 Cookie
这取决于前端形态。
如果是纯 API + 前端应用,很多项目会放内存或 localStorage,但要认真处理 XSS 风险。
如果你用 Cookie,建议设置 HttpOnly、Secure、SameSite,同时处理 CSRF。没有绝对通用答案,要看威胁模型。
SECRET_KEY 怎么生成
可以用 Python 生成:
python -c "import secrets; print(secrets.token_urlsafe(32))"然后放到环境变量:
export SECRET_KEY="生成的随机字符串"生产环境不要提交到 Git。
总结
FastAPI 的登录鉴权并不复杂,真正要注意的是边界:
密码只存哈希JWT 设置过期时间密钥从环境变量读取受保护接口统一走依赖业务逻辑不要塞进路由函数本文示例适合用来理解流程,不适合原封不动搬到生产。下一步可以把内存字典换成数据库,再补 refresh token、权限角色和测试。认证规范细节可以参考 FastAPI 安全教程 和 JWT 标准 RFC 7519。