文章列表
4 分钟阅读

FastAPI 项目实战:用户登录与 JWT 鉴权


系列:Python Web 框架

第 9 / 11 篇

  1. FastAPI 安装与使用指南
  2. FastAPI 进阶使用指南
  3. Django 安装与使用指南
  4. Django 进阶使用指南
  5. Django 与 FastAPI 对比:如何选择合适的 Python Web 框架
  6. Python Web 框架有哪些,怎么选
  7. Flask 安装与使用指南
  8. Django REST Framework 入门:把 Django 做成 API
  9. FastAPI 项目实战:用户登录与 JWT 鉴权
  10. Python Web 项目部署指南:Gunicorn、Uvicorn、Nginx 和 Docker
  11. Python Web 项目结构怎么设计

很多 FastAPI 教程会停在“写几个路由”和“自动文档很好用”。但真实 API 项目绕不开登录。

这篇不做完整用户中心,只写一个最小但靠谱的 JWT 鉴权流程:

注册用户
密码哈希入库
登录校验密码
签发 access token
访问受保护接口

示例为了聚焦流程,用内存字典模拟数据库。真实项目把 fake_users_db 换成数据库查询即可。FastAPI 基础部分可以先看 FastAPI 安装与使用指南,依赖注入和项目结构可以接着看 FastAPI 进阶使用指南

安装依赖

创建项目:

Terminal window
mkdir fastapi-auth-demo
cd fastapi-auth-demo
python3 -m venv .venv
source .venv/bin/activate

安装依赖:

Terminal window
pip install fastapi uvicorn[standard] python-jose[cryptography] passlib[bcrypt] python-multipart

几个包的作用:

fastapi Web 框架
uvicorn ASGI 服务器
python-jose 生成和解析 JWT
passlib[bcrypt] 密码哈希
python-multipart 支持 OAuth2PasswordRequestForm 表单

项目结构

先用一个小结构,不要一上来拆太细:

fastapi-auth-demo/
├── main.py
├── auth.py
└── schemas.py

后续接数据库时,再拆 models.pydatabase.pyrepositories/

定义请求和响应模型

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,还需要装:

Terminal window
pip install pydantic[email]

这里没有返回用户 id,是为了让示例更短。真实项目通常会有 idcreated_atis_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, timezone
from 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, status
from fastapi.security import OAuth2PasswordRequestForm
from fastapi import Depends
from auth import create_access_token, hash_password, verify_password
from 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 规范里的历史包袱。

启动:

Terminal window
uvicorn main:app --reload

注册:

Terminal window
curl -X POST http://127.0.0.1:8000/register \
-H 'Content-Type: application/json' \
-d '{"email":"alice.local@test","password":"secret123"}'

登录:

Terminal window
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, status
from fastapi.security import OAuth2PasswordBearer
from 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 exc

main.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"]}

调用:

Terminal window
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 设成几个月。泄露之后风险太大。

这取决于前端形态。

如果是纯 API + 前端应用,很多项目会放内存或 localStorage,但要认真处理 XSS 风险。

如果你用 Cookie,建议设置 HttpOnlySecureSameSite,同时处理 CSRF。没有绝对通用答案,要看威胁模型。

SECRET_KEY 怎么生成

可以用 Python 生成:

Terminal window
python -c "import secrets; print(secrets.token_urlsafe(32))"

然后放到环境变量:

Terminal window
export SECRET_KEY="生成的随机字符串"

生产环境不要提交到 Git。

总结

FastAPI 的登录鉴权并不复杂,真正要注意的是边界:

密码只存哈希
JWT 设置过期时间
密钥从环境变量读取
受保护接口统一走依赖
业务逻辑不要塞进路由函数

本文示例适合用来理解流程,不适合原封不动搬到生产。下一步可以把内存字典换成数据库,再补 refresh token、权限角色和测试。认证规范细节可以参考 FastAPI 安全教程JWT 标准 RFC 7519