给 FastAPI 加上登录:从 Argon2 密码哈希到 JWT
摘要:完成注册、登录、Bearer Token 和当前用户接口,解释密码哈希、JWT 签名与过期时间各自解决什么,并验证错误密码和伪造令牌会被拒绝。

图:密码经单向哈希后保存,登录成功再签发令牌,受保护路由负责验签与过期校验。
任务开始属于不同用户后,服务必须先回答“调用者是谁”。最常见的错误是把明文密码存进数据库,或者把 JWT 当成加密容器,往里面塞隐私数据。
下面搭一条最小但完整的认证链路:注册时用 Argon2 哈希密码,登录时验证哈希,成功后签发带过期时间的 JWT,再用 Bearer Token 访问 /users/me。
先完成注册、登录和带 Token 访问
fastapi dev examples/ch11_auth/main.py
curl -X POST http://127.0.0.1:8000/register \
-H "Content-Type: application/json" \
-d '{"username":"alice","password":"safe-password"}'
curl -X POST http://127.0.0.1:8000/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d 'username=alice&password=safe-password'
预期注册返回用户名但不返回密码,登录返回 access_token 和 bearer 类型。
先划清教学示例和生产认证的边界
cd 04-fastapi-beginner
source .venv/bin/activate
python -c "import jwt, pwdlib; print('security dependencies ready')"
本文内存用户库只用于学习认证流程。生产应用必须使用数据库、环境变量密钥、HTTPS、速率限制和可靠的密钥轮换流程。
把注册、签发和鉴权连成一条链
哈希密码
password_hash = PasswordHash.recommended()
users[payload.username] = password_hash.hash(payload.password)
登录时使用 verify(明文输入, 已存哈希)。哈希不是加密,不能还原原密码;数据库泄漏时它能显著提高攻击成本。
签发 Token
expires_at = datetime.now(timezone.utc) + timedelta(minutes=30)
token = jwt.encode(
{"sub": username, "exp": expires_at},
SECRET_KEY,
algorithm="HS256",
)
JWT 是签名的,不是加密的。不要把密码、隐私或密钥放进 payload。
保护接口
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/token")
def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]):
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
...
Token 缺失、签名错误、过期或用户不存在都返回 401,并携带 WWW-Authenticate: Bearer。

图:系统接收明文密码后只保存单向哈希结果,数据库里不应出现明文。
正确 Token 和伪造 Token 都要试
从登录响应复制 Token:
curl -i -H "Authorization: Bearer 你的Token" \
http://127.0.0.1:8000/users/me
curl -i -H "Authorization: Bearer invalid" \
http://127.0.0.1:8000/users/me
预期第一条为 200,第二条为 401。也可以打开 /docs,先执行 /token,再点击右上角 Authorize。
python -m pytest tests/test_ch11.py -q
JWT 能证明内容未被篡改,但不会隐藏内容
认证链路是:注册时哈希 → 登录时验证哈希 → 签发短期 Token → 客户端每次携带 Token → 服务校验签名和过期时间 → 得到当前用户。
sub 表示令牌主体。这里只放用户名,综合项目会改为稳定的用户 ID。exp 让泄漏令牌不会永久有效,但真正的注销、撤销和刷新令牌还需要额外设计。
认证回答“你是谁”,授权回答“你能做什么”。能通过 /users/me 不代表可以读取其他人的任务,最终项目会实现所有者过滤。

图:签名用于证明令牌未被篡改,不会隐藏内容;过期令牌同样必须拒绝。
登录接口最容易留下的安全问题
- 把登录请求发成 JSON:OAuth2PasswordRequestForm 使用表单编码。
- 数据库保存明文密码:任何日志、备份或泄漏都会直接暴露用户密码。
- JWT payload 放敏感信息:Base64URL 内容可以被任何持有者读取。
- 密钥硬编码进入生产:示例密钥只能本地使用,生产从秘密管理系统注入。
- 捕获所有异常并返回 200:认证失败应稳定返回 401。
从短期 Token 继续走向完整会话管理
- 增加重复注册测试,预期 409。
- 把 Token 有效期改为 1 秒,等待后验证过期结果。
- 解码 JWT payload,证明内容可见但篡改后签名失效。
- 参考 配置管理实践,把 SECRET_KEY 移入 Settings。
- 设计刷新令牌表至少需要哪些字段。
最后,用测试固定这次改动
手动请求通过后,运行与本文对应的自动化测试:
python -m pytest tests/test_ch11.py -q
python tests/validate_course.py
写在最后
你已经实现标准 Bearer Token 认证闭环。下一篇不再增加接口功能,而是把测试提升为稳定工程体系:fixture、参数化、依赖替换和测试隔离。