
文章目录
1. 课前导读
本节课学习目标
在前16课中,我们已经构建了一个功能完整的API系统,包含数据库CRUD、分页筛选、异步并发优化等。但一个真实的应用必须解决安全问题——如何识别用户身份,并控制不同用户对不同资源的访问权限? 如果任何人都能随意创建、修改、删除数据,系统将毫无安全性可言。
本节课将带你实现基于JWT(JSON Web Token) 的用户认证与授权机制。学完本节课,你将:
- 理解JWT原理:了解JWT的结构、签名算法、以及为什么适合分布式系统
- 实现登录接口:验证用户名密码,生成并返回JWT access token
- 保护API接口:使用依赖注入验证JWT,获取当前登录用户
- 实现角色权限控制:区分普通用户和管理员,限制接口访问
- 实现Token刷新机制:使用refresh token延长登录状态
- 掌握安全最佳实践:密码加密存储、Token过期时间、HTTP-only Cookie传输(可选)
前置知识
- 已完成第15课,掌握数据库用户模型和密码加密(passlib)
- 了解依赖注入(第10课)和异步编程基础(第16课)
- 对HTTP认证头(Authorization: Bearer )有基本概念
学完能掌握什么
学完本节课后,你将具备以下能力:
- 设计安全的登录/注册流程:密码加密、JWT生成与验证
- 使用FastAPI的
HTTPBearer安全方案:自动处理Authorization头,并生成Swagger文档的“Authorize”按钮 - 编写可复用的认证依赖:
get_current_user依赖自动从请求中提取并验证token,返回用户对象 - 实现基于角色的权限控制:通过
Depends组合,限制特定接口仅管理员可访问 - 部署Token刷新机制:使用refresh token实现无感刷新,提升用户体验
适用人群
- 需要为API添加用户登录和权限控制的开发者
- 准备构建多用户系统(如电商、社交、管理后台)的工程师
- 对JWT认证原理和最佳实践感兴趣的后端开发
- 希望学习FastAPI中
Security模块用法的进阶学员
2. 核心理论讲解
2.1 为什么需要认证与授权?
- 认证(Authentication):确认用户身份。例如,登录时验证用户名和密码,返回令牌。
- 授权(Authorization):确定用户能做什么。例如,普通用户可以查看自己的订单,管理员可以查看所有订单。
在Web API中,最常见的认证方式是基于Token的无状态认证。服务器不保存用户登录状态,而是签发一个加密的Token给客户端,客户端在后续请求中携带Token,服务器验证Token的合法性。
2.2 JWT(JSON Web Token)详解
JWT结构:xxxxx.yyyyy.zzzzz,由三部分组成,用.分隔:
- Header(头部):包含令牌类型(JWT)和签名算法(如HS256)。
- Payload(负载):包含声明(claims),例如用户ID、用户名、过期时间等。
- Signature(签名):对Header和Payload使用密钥签名,防止篡改。
为什么选择JWT?
- 自包含:用户信息存储在Token中,无需查询数据库即可获取用户标识。
- 分布式友好:服务器不存储session,便于横向扩展。
- 跨语言:任何语言都有JWT库。
注意:Payload是Base64Url编码的,不要存储敏感信息(如密码),因为客户端可以解码查看。
2.3 JWT在FastAPI中的工作流程
- 用户通过
POST /login提交用户名和密码。 - 服务器验证凭据,生成JWT(包含用户ID、角色、过期时间),返回给客户端。
- 客户端将JWT存储在localStorage或Cookie中,后续请求在
Authorization: Bearer <token>头中携带。 - FastAPI使用
HTTPBearer安全方案提取token,通过依赖函数验证签名和过期时间,解析出用户信息。 - 路由函数通过
Depends(get_current_user)获取当前用户对象,进行权限判断。
2.4 密码加密:为什么不存明文?
数据库泄露是常见安全事件。如果密码明文存储,攻击者可获取所有用户密码。应使用单向哈希加盐存储。本课使用passlib库,它封装了bcrypt算法,自动加盐。
2.5 Access Token与Refresh Token
- Access Token:短期有效(如15分钟),用于访问受保护资源。
- Refresh Token:长期有效(如7天),用于在Access Token过期后获取新的Access Token。
为什么要区分?Access Token暴露在每次请求中,有效期短可降低泄露风险;Refresh Token只在刷新端点传输,更安全。
2.6 与Flask/Django的认证对比
| 框架 | 常用认证方案 | JWT集成 | 权限控制 | 文档支持 |
|---|---|---|---|---|
| FastAPI | HTTPBearer + 自定义依赖 | python-jose | 依赖注入组合 | Swagger自动集成 |
| Flask | Flask-JWT-Extended | 有扩展 | 装饰器 | 手动 |
| Django | django-rest-framework-simplejwt | 有库 | 权限类 | DRF文档 |
FastAPI的安全方案与OpenAPI规范无缝集成,自动生成文档中的“Authorize”按钮。
3. 环境搭建 & 实操准备
3.1 复用项目环境
继续使用第16课的项目。确保虚拟环境已激活。
cd ~/fastapi_course/lesson03_first_project
source venv/bin/activate
3.2 安装JWT和密码加密依赖
pip install python-jose[cryptography] # JWT生成与验证
pip install passlib[bcrypt] # 密码哈希(已在之前安装,确保)
pip install python-multipart # 用于OAuth2密码流程(依赖)
3.3 更新数据库模型(如果需要)
我们已经在用户模型中有hashed_password、username、role等字段,无需修改。但需要确保用户表中有测试数据。可以使用之前注册接口创建。
3.4 新增代码文件
创建app/api/v1/auth.py,并在main.py中注册。
touch app/api/v1/auth.py
同时,创建app/core/security.py存放安全相关工具函数。
mkdir -p app/core
touch app/core/__init__.py
touch app/core/security.py
main.py中注册:
from app.api.v1 import auth
app.include_router(auth.router, prefix="/api/v1/auth", tags=["认证授权"])
3.5 环境变量配置
在.env中添加JWT相关配置:
SECRET_KEY=your-secret-key-please-change-in-production-at-least-32-chars
ALGORITHM=HS256
ACCESS_TOKEN_EXPIRE_MINUTES=30
REFRESH_TOKEN_EXPIRE_DAYS=7
注意:生产环境SECRET_KEY必须随机且足够长,不能泄露。可以使用openssl rand -hex 32生成。
更新app/config.py读取这些变量:
class Settings(BaseSettings):
# ... 原有配置 ...
secret_key: str = "your-secret-key-change-me"
algorithm: str = "HS256"
access_token_expire_minutes: int = 30
refresh_token_expire_days: int = 7
4. 手把手代码实战
4.1 实现密码加密和验证工具
在app/core/security.py中:
# app/core/security.py
from passlib.context import CryptContext
from datetime import datetime, timedelta
from typing import Optional
from jose import JWTError, jwt
from app.config import settings
# 密码加密上下文
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def hash_password(password: str) -> str:
"""对密码进行哈希"""
return pwd_context.hash(password)
def verify_password(plain_password: str, hashed_password: str) -> bool:
"""验证密码是否匹配"""
return pwd_context.verify(plain_password, hashed_password)
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
"""生成Access Token"""
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=settings.access_token_expire_minutes)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, settings.secret_key, algorithm=settings.algorithm)
return encoded_jwt
def create_refresh_token(data: dict) -> str:
"""生成Refresh Token(有效期更长)"""
expire = datetime.utcnow() + timedelta(days=settings.refresh_token_expire_days)
to_encode = data.copy()
to_encode.update({"exp": expire, "type": "refresh"})
return jwt.encode(to_encode, settings.secret_key, algorithm=settings.algorithm)
def decode_token(token: str) -> Optional[dict]:
"""解码并验证JWT,返回payload或None(如果无效)"""
try:
payload = jwt.decode(token, settings.secret_key, algorithms=[settings.algorithm])
return payload
except JWTError:
return None
4.2 实现登录接口(生成Token)
在app/api/v1/auth.py中创建登录路由:
# app/api/v1/auth.py
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from sqlalchemy.ext.asyncio import AsyncSession
from pydantic import BaseModel
from typing import Optional
from app.database import get_db
from app.services.user_service import user_service
from app.core.security import verify_password, create_access_token, create_refresh_token, decode_token
from app.config import settings
router = APIRouter()
# OAuth2密码流方案,用于Swagger文档中的授权按钮
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login")
# 响应模型
class Token(BaseModel):
access_token: str
refresh_token: str
token_type: str = "bearer"
class TokenData(BaseModel):
username: Optional[str] = None
user_id: Optional[int] = None
@router.post("/login", response_model=Token)
async def login(
form_data: OAuth2PasswordRequestForm = Depends(),
db: AsyncSession = Depends(get_db)
):
"""
OAuth2兼容的登录接口,支持表单格式
- username: 用户名
- password: 密码
"""
# 查找用户
user = await user_service.get_by_username(db, form_data.username)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="用户名或密码错误",
headers={"WWW-Authenticate": "Bearer"},
)
# 验证密码
if not verify_password(form_data.password, user.hashed_password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="用户名或密码错误",
headers={"WWW-Authenticate": "Bearer"},
)
# 生成Token
access_token = create_access_token(
data={"sub": user.username, "user_id": user.id, "role": user.role}
)
refresh_token = create_refresh_token(
data={"sub": user.username, "user_id": user.id}
)
return {"access_token": access_token, "refresh_token": refresh_token, "token_type": "bearer"}
说明:
OAuth2PasswordRequestForm是FastAPI提供的依赖,自动解析application/x-www-form-urlencoded格式的表单(username和password字段)。- 该依赖也会让Swagger UI自动生成“Authorize”按钮,方便测试。
4.3 实现获取当前用户的依赖
为了在受保护的路由中获取当前用户,我们需要编写依赖函数,从请求头提取Token,验证并返回用户对象。
在app/api/v1/auth.py中继续添加:
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from sqlalchemy.ext.asyncio import AsyncSession
from jose import JWTError
# 重写oauth2_scheme(或使用全局定义)
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login")
async def get_current_user(
token: str = Depends(oauth2_scheme),
db: AsyncSession = Depends(get_db)
):
"""
从请求头中提取JWT,验证并返回当前用户对象
如果无效,抛出401异常
"""
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="无效的认证凭据",
headers={"WWW-Authenticate": "Bearer"},
)
payload = decode_token(token)
if payload is None:
raise credentials_exception
user_id: int = payload.get("user_id")
if user_id is None:
raise credentials_exception
# 可选:从数据库获取最新用户信息(确保用户仍存在且激活)
user = await user_service.get(db, id=user_id)
if user is None or not user.is_active:
raise credentials_exception
return user
async def get_current_active_user(current_user = Depends(get_current_user)):
"""验证用户是否激活(额外的安全检查)"""
if not current_user.is_active:
raise HTTPException(status_code=400, detail="用户已被禁用")
return current_user
async def get_current_admin_user(current_user = Depends(get_current_active_user)):
"""仅管理员可访问的依赖"""
if current_user.role != "admin":
raise HTTPException(status_code=403, detail="权限不足,需要管理员角色")
return current_user
4.4 使用依赖保护接口
创建一个受保护的路由,只需要登录即可访问:
@router.get("/me", response_model=UserResponse)
async def read_users_me(current_user: User = Depends(get_current_active_user)):
"""获取当前登录用户信息,需要认证"""
return current_user
创建一个仅管理员可访问的路由:
@router.get("/admin-only")
async def admin_endpoint(current_admin: User = Depends(get_current_admin_user)):
"""仅管理员可访问的示例接口"""
return {"message": f"欢迎管理员 {current_admin.username}"}
4.5 实现Token刷新接口
当Access Token过期时,客户端可以使用Refresh Token换取新的Access Token。
class RefreshTokenRequest(BaseModel):
refresh_token: str
@router.post("/refresh", response_model=Token)
async def refresh_access_token(refresh_req: RefreshTokenRequest):
"""使用Refresh Token获取新的Access Token和Refresh Token"""
payload = decode_token(refresh_req.refresh_token)
if payload is None or payload.get("type") != "refresh":
raise HTTPException(status_code=401, detail="无效的Refresh Token")
user_id = payload.get("user_id")
username = payload.get("sub")
if not user_id or not username:
raise HTTPException(status_code=401, detail="无效的Token内容")
# 生成新token(可选:可以检查用户状态,但为简化省略)
new_access_token = create_access_token(
data={"sub": username, "user_id": user_id, "role": payload.get("role", "user")}
)
new_refresh_token = create_refresh_token(
data={"sub": username, "user_id": user_id}
)
return {
"access_token": new_access_token,
"refresh_token": new_refresh_token,
"token_type": "bearer"
}
4.6 集成Swagger文档的认证按钮
由于我们使用了OAuth2PasswordBearer,FastAPI会自动在/docs页面添加“Authorize”按钮。但需要指定tokenUrl为我们的登录接口。已经通过tokenUrl="/api/v1/auth/login"实现。用户点击按钮,输入用户名密码,即可自动在后续请求中添加Authorization: Bearer <token>头。
4.7 实战:为用户资源接口加上权限控制
修改第15课的用户CRUD路由,添加认证和授权:
# 在 user_crud.py 中引入依赖
from app.api.v1.auth import get_current_active_user, get_current_admin_user
# 获取用户列表(仅管理员可访问)
@router.get("/", response_model=Page[UserResponse])
async def get_users(
# ... 原有参数 ...
current_admin: User = Depends(get_current_admin_user) # 新增依赖
):
# 原有逻辑
pass
# 获取当前登录用户自己的信息(普通用户可访问)
@router.get("/me", response_model=UserResponse)
async def get_my_info(current_user: User = Depends(get_current_active_user)):
return current_user
# 更新用户(普通用户只能更新自己,管理员可以更新任意用户)
@router.patch("/{user_id}", response_model=UserResponse)
async def update_user(
user_id: int,
user_update: UserUpdate,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
# 检查权限:只有管理员或者更新自己的信息
if current_user.id != user_id and current_user.role != "admin":
raise HTTPException(status_code=403, detail="无权修改其他用户信息")
# ... 更新逻辑
4.8 注销登录(前端处理)
JWT是无状态的,服务器不需要主动注销。前端只需删除本地存储的Token即可。如果需要服务器端主动撤销Token(如用户修改密码后),需要维护Token黑名单(使用Redis),本课不展开。
4.9 安全增强:使用HTTP-only Cookie传输Token(可选)
将Access Token存储在localStorage存在XSS风险。更安全的方式是使用HTTP-only Cookie,无法被JavaScript读取,可防止XSS攻击。但Cookie易受CSRF攻击,需配合CSRF Token。因篇幅,本课不展开,可自行研究。
4.10 测试认证流程
启动应用:
uvicorn app.main:app --reload
步骤1:注册用户(如果还没有)
curl -X POST http://localhost:8000/api/v1/users/ \
-H "Content-Type: application/json" \
-d '{"username": "alice", "email": "alice@ex.com", "password": "123456", "age": 25}'
步骤2:登录获取Token
curl -X POST http://localhost:8000/api/v1/auth/login \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "username=alice&password=123456"
响应示例:
{
"access_token": "eyJhbG...",
"refresh_token": "eyJhbG...",
"token_type": "bearer"
}
步骤3:访问受保护接口
curl -H "Authorization: Bearer eyJhbG..." http://localhost:8000/api/v1/auth/me
步骤4:访问仅管理员接口(如果alice不是管理员,会返回403)
4.11 在Swagger UI中测试
打开http://localhost:8000/docs,点击右上角“Authorize”按钮,输入用户名和密码,即可授权。然后尝试调用/auth/me接口,会看到成功响应。
5. 重点知识点总结
JWT核心语法速查表
| 操作 | 函数/方法 |
|---|---|
| 哈希密码 | hash_password(plain) |
| 验证密码 | verify_password(plain, hashed) |
| 生成Access Token | create_access_token(data, expires_delta) |
| 生成Refresh Token | create_refresh_token(data) |
| 解码Token | jwt.decode(token, secret, algorithms) |
| 提取Bearer Token | OAuth2PasswordBearer(tokenUrl=...) |
| 获取当前用户依赖 | get_current_user |
| 权限控制依赖 | get_current_admin_user |
安全最佳实践
- 密钥管理:
SECRET_KEY必须复杂且保密,使用环境变量注入,不要硬编码。 - Token有效期:Access Token建议15-30分钟,Refresh Token 7天,平衡安全与体验。
- 密码强度:要求至少8位,包含字母和数字。
- 使用HTTPS:生产环境必须启用HTTPS,防止Token被中间人窃取。
- Token黑名单:如果需要强制注销,维护Redis黑名单。
- Payload最小化:只存储必要信息(用户ID、角色),不存储敏感数据。
易错点与避坑指南
-
❌ 将密码明文存储在数据库中
必须使用hash_password哈希后存储。 -
❌ 在JWT Payload中存储密码或其他敏感信息
JWT的Payload可解码,任何人都能看到。只存非敏感标识。 -
❌ 未验证Token的签名和过期时间
解码时必须捕获JWTError,并检查exp声明。 -
❌ 在依赖中直接返回用户对象,未检查用户是否仍存在于数据库
用户可能被删除或禁用,最好每次都查询数据库确认状态。 -
❌ 在
OAuth2PasswordBearer中错误配置tokenUrl
导致Swagger授权无法正常获取Token。必须与登录接口路径一致。 -
❌ 将Refresh Token与Access Token使用相同过期时间
失去了刷新机制的意义。Refresh Token有效期应远大于Access Token。
依赖注入组合模式
# 组合依赖,实现递进式权限
get_current_user → get_current_active_user → get_current_admin_user
每个依赖都可以单独用于不同敏感度的接口。
6. 课后作业 & 思考题
实操练习题(必做)
-
基础练习:完成登录、获取当前用户、刷新Token接口,并测试
- 创建一个普通用户登录,获取Token。
- 调用
/auth/me接口验证能正确返回用户信息。 - 等待Access Token过期(或修改配置为1分钟),使用Refresh Token获取新Token。
-
进阶练习:实现基于角色的接口权限控制
- 在用户表中添加
role字段(已有) - 创建接口
GET /admin/users,仅管理员可访问,返回所有用户列表。 - 创建接口
DELETE /users/{user_id},仅管理员可删除。 - 普通用户请求这些接口返回403。
- 在用户表中添加
-
挑战练习:实现Token黑名单(使用Redis)
- 安装
aioredis,在登出接口中将Token加入黑名单(设置过期时间等于Token剩余有效期)。 - 修改
get_current_user依赖,在验证Token后检查是否在黑名单中。
- 安装
理论思考题
- JWT相比传统的Session认证有什么优缺点? 在分布式系统中为何更受欢迎?
- 为什么Access Token有效期要设置得短,而Refresh Token有效期长? 如果Refresh Token被盗,攻击者可以不断刷新,如何防护?
OAuth2PasswordBearer的tokenUrl参数作用是什么? 它与在Swagger UI中显示“Authorize”按钮有什么关系?- 如何实现“踢出用户”功能(强制指定用户下线)? 在不使用黑名单的情况下是否可能?
拓展学习方向
- OAuth2.0与OpenID Connect:了解更完整的认证授权协议
- 使用
python-jose的更多算法:RS256非对称加密 - 使用
fastapi.security中的HTTPBearer和APIKeyHeader:其他安全方案 - 单点登录(SSO):集成OAuth2社交登录(Google、GitHub)
7. 本节干货总结
核心考点(面试/自测)
-
如何实现JWT认证?
用户登录验证后,生成JWT返回;客户端在请求头Authorization: Bearer <token>携带;服务端使用依赖提取并验证。 -
FastAPI中
OAuth2PasswordBearer的作用?
从请求头提取Token,并自动生成Swagger文档的认证按钮。 -
如何保护一个路由,仅允许管理员访问?
定义依赖get_current_admin_user,在其中检查用户角色,失败抛出403异常。 -
Refresh Token的用途?
在Access Token过期后,使用Refresh Token获取新的Access Token,避免用户频繁登录。 -
如何存储密码才安全?
使用bcrypt等算法加盐哈希,永远不存储明文。
实际工作应用场景
-
场景1:用户登录与身份保持
移动App或前端应用,用户登录后获取Token,后续请求携带Token,后端识别用户。 -
场景2:管理后台权限分级
普通员工只能查看数据,管理员可以编辑和删除。通过角色依赖轻松实现。 -
场景3:API开放平台
为第三方应用颁发JWT,通过自定义声明控制其访问的API范围和有效期。 -
场景4:微服务间认证
服务A生成JWT,服务B验证JWT,实现无状态安全通信。 -
场景5:单页应用(SPA)
前端存储Token,每次请求自动添加到Authorization头,实现无刷新登录。
下节课预告
本节课我们构建了完整的用户认证与授权体系,API具备了身份识别和权限控制能力。但当一个系统用户量增长,频繁的数据库查询(如每次请求都查用户表)可能会成为性能瓶颈。此时,引入缓存可以大幅提升响应速度。
下节课(第18课),我们将学习:
- 集成Redis缓存:安装配置、异步客户端使用
- 接口限流:防止恶意刷接口,保护系统稳定性
- 会话存储:使用Redis存储用户会话或临时数据
- 性能优化实战:缓存数据库查询结果、使用缓存提升响应速度
让你的API在高并发下依然稳健!下节课见!
🔗《20节课 FastAPI 从入门到精通》系列课程导航
🌟 感谢您耐心阅读到这里!
💡 如果本文对您有所启发欢迎:
👍 点赞📌 收藏 📤 分享给更多需要的伙伴。
🗣️ 期待在评论区看到您的想法, 共同进步。
🔔 关注我,持续获取更多干货内容~
🤗 我们下篇文章见~

471

被折叠的 条评论
为什么被折叠?



