在前几篇中,我们构建的 FastAPI 和 PostgreSQL 应用完全开放——任何人都能访问所有接口。现实中的 Web 应用需要区分"谁能进来"和"进来后能做什么",这就是认证(Authentication)和授权(Authorization)要解决的问题。
认证(Authentication,缩写 AuthN)回答"你是谁"——验证用户的身份是否合法。你输入账号密码登录网站,服务器核对密码正确性,这个过程就是认证。
授权(Authorization,缩写 AuthZ)回答"你能做什么"——在确认身份之后,判断该用户是否有权限执行某个操作。普通用户不能访问管理后台、游客不能发帖,这些都是授权控制。
打个比方:你拿着小区门禁卡刷卡进大门,门禁系统验证卡片有效性——这是认证。进了大门后,你的卡只能开 3 号楼 502 室的门,打不开物业办公室——这是授权。认证是授权的前提,没有认证就谈不上授权。
| 维度 | 认证(AuthN) | 授权(AuthZ) |
|---|---|---|
| 核心问题 | 你是谁? | 你能做什么? |
| 触发时机 | 登录时 | 每次请求时 |
| 常见实现 | 账号密码、OAuth、生物识别 | RBAC 角色控制、ACL 权限列表 |
| 失败结果 | 401 Unauthorized | 403 Forbidden |
理解了认证和授权的区别后,我们来看最早的 Web 认证方案——Cookie + Session。这套机制至今仍是传统 Web 应用的主流,理解它是掌握 JWT 的前提。
Cookie 是浏览器存储的一小段文本数据(每条不超过 4KB),由服务器通过 HTTP 响应头 Set-Cookie 下发。此后浏览器每次请求同一域名时,会自动在 Cookie 请求头中原样带回。
用 curl 观察服务器下发 Cookie 的过程:
这个 sessionid=abc123xyz789 就是 Cookie 的键值对。浏览器收到后会自动保存,并在后续请求中携带。
Cookie 只是一个传输通道,真正存储用户登录状态的是服务器端的 Session。Session 的本质是一个以 Session ID 为键的字典,存储在服务器内存或 Redis 等外部存储中。
服务器端的 Session 存储结构类似于这样的 Python 字典:
这种方案称为有状态认证——服务器必须记住每个用户的 Session。当用户量增大时,Session 存储成为瓶颈。多台服务器部署时还需要解决 Session 共享问题(通常用 Redis 统一存储),架构复杂度显著上升。
| 特性 | Session(有状态) | JWT(无状态) |
|---|---|---|
| 状态存储 | 服务器内存/Redis | 客户端 Token 中 |
| 扩展性 | 需 Session 共享 | 天然支持分布式 |
| 主动注销 | 删除即可立即失效 | 需额外黑名单机制 |
| 适用场景 | 传统 Web 应用 | API、移动端、微服务 |
Session 的核心痛点是"服务器要记住所有人"。JWT(JSON Web Token)换了个思路:把用户信息直接编码进一个签名字符串发给客户端,服务器不需要存储任何状态——这就是无状态认证。
一个 JWT 看起来像这样(三段用点号分隔):
每一段都是 Base64URL 编码的 JSON。把前两段解码后能看到清晰的内容:
Payload 中的常用字段(称为 Claim)有约定俗成的缩写:sub(subject,用户标识)、exp(expiration,过期时间)、iat(issued at,签发时间)。
第三段 Signature 是防篡改的关键。服务器用密钥对 Header + Payload 做哈希签名,收到 Token 时重新计算签名来验证完整性。两种最常用的算法各有适用场景:
| 特性 | HS256(对称) | RS256(非对称) |
|---|---|---|
| 密钥类型 | 单个密钥(签名+验证共用) | 公钥/私钥对 |
| 适用规模 | 单体应用、个人项目 | 微服务、多服务验证 |
| 性能 | 快(HMAC 计算) | 稍慢(RSA 运算) |
| 密钥分发 | 所有服务共享同一密钥 | 私钥签名,公钥验证,公钥可公开 |
本篇使用 HS256——对于单体 FastAPI 应用足够安全且性能最优。当你将来构建微服务架构时,再切换到 RS256,让认证服务持有私钥签发 Token,其他服务用公钥验证。
PyJWT 是 Python 生态最成熟的 JWT 库,当前最新版本 2.13.0(2026 年 5 月发布的安全更新版本,修复了 5 个安全漏洞,建议所有用户升级)。安装并试用:
jwt.decode() 会自动检查 exp 字段,Token 过期后抛出 jwt.ExpiredSignatureError 异常。签名不匹配则抛出 jwt.InvalidSignatureError。这些异常在 FastAPI 中会被捕获并转换为 401 响应。
在实现登录之前,必须先解决密码存储问题。数据库里绝不能存明文密码——一旦数据库泄露,所有用户密码直接暴露。
密码哈希(Hash)是一种单向函数:把任意长度的密码映射为固定长度的字符串,且无法从哈希值反推出原始密码。注册时存储哈希值,登录时对用户输入的密码做同样的哈希,比对两个哈希值是否一致。
注意哈希和加密的区别:加密是双向的(可以解密),哈希是单向的(不可逆)。密码必须用哈希而非加密——即使服务器被攻破,攻击者也无法还原原始密码。
过去 Python 生态的密码哈希标准库是 passlib,但它自 2020 年起停止维护,且在 Python 3.13+ 上因 crypt 模块被移除而直接崩溃。FastAPI 官方文档现已将推荐方案替换为 pwdlib,使用 Argon2 算法。
注意哈希值中的 $argon2id$ 前缀——Argon2id 是 Argon2 的推荐变体,结合了抗侧信道攻击和抗 GPU 破解两种特性。pwdlib 每次哈希会自动生成随机盐值(salt),所以同一个密码两次哈希的结果不同,防止彩虹表攻击。
掌握了 JWT 和密码哈希两块基石后,现在把所有零件组装起来。我们将在 FastAPI 中实现完整的用户注册、登录和 Token 签发流程,为下一篇的博客系统打好认证基础。
这里用 aiosqlite 做演示数据库(SQLite 的异步驱动),省去 PostgreSQL 安装步骤。生产环境替换连接字符串即可无缝切换到 PostgreSQL。
注意登录接口的安全细节:无论用户名不存在还是密码错误,都返回同样的"用户名或密码错误"——不告诉攻击者具体是哪个错了,防止用户名枚举攻击。
启动服务并测试注册和登录:
登录成功后返回 JSON:
用户注册和登录已经跑通,但现在所有接口仍然是对外开放的。我们需要一种机制让特定接口"只允许已登录用户访问"。FastAPI 通过 OAuth2PasswordBearer + 依赖注入优雅地解决了这个问题。
OAuth2PasswordBearer 是 FastAPI 提供的安全工具类。它的核心作用是从请求头 Authorization: Bearer <token> 中自动提取 JWT Token。tokenUrl 参数告诉 Swagger UI 登录接口的地址,这样在文档页面就能直接测试完整流程。
客户端访问受保护接口时,必须在请求头中携带 Token:
在 main.py 中添加 OAuth2 认证依赖和受保护接口:
整个保护流程的关键在于 Depends(get_current_user) 这一行。FastAPI 的依赖注入系统会自动执行 get_current_user 函数——它先用 oauth2_scheme 从请求头提取 Token,再调用 decode_access_token 验证签名和过期时间。任何步骤失败都会抛出 401 异常,请求不会到达路由函数。
用 Token 访问受保护接口:
打开 http://localhost:8000/docs 的 Swagger UI 页面,你会看到右上角出现了 "Authorize" 按钮。点击后输入用户名密码,Swagger 自动完成登录并在后续请求中携带 Token——这就是 tokenUrl="token" 参数的魔力。
认证系统的安全性取决于最薄弱的环节。以下是生产环境中必须遵守的安全准则。
openssl rand -hex 32 生成。CORSMiddleware 限制允许的来源域名。禁止 allow_origins=["*"] 搭配 allow_credentials=True——这等于完全放开认证。os.environ.get("SECRET_KEY") 从环境变量读取,.env 文件加入 .gitignore
/users/me 接口,确认返回正确的用户名。再尝试不带 Token 访问,确认返回 401。PUT /users/me/password 接口,要求用户先登录(携带 Token),然后验证旧密码正确后更新为新密码。提示:需要同时使用 Depends(get_current_user) 和请求体中的旧密码字段。POST /refresh 接口,客户端用 refresh_token 换取新的 access_token。提示:两个 Token 的 Payload 中加入 "type": "access" 或 "type": "refresh" 区分用途,验证时检查类型是否匹配。