给 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_tokenbearer 类型。

先划清教学示例和生产认证的边界

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 继续走向完整会话管理

  1. 增加重复注册测试,预期 409。
  2. 把 Token 有效期改为 1 秒,等待后验证过期结果。
  3. 解码 JWT payload,证明内容可见但篡改后签名失效。
  4. 参考 配置管理实践,把 SECRET_KEY 移入 Settings。
  5. 设计刷新令牌表至少需要哪些字段。

最后,用测试固定这次改动

手动请求通过后,运行与本文对应的自动化测试:

python -m pytest tests/test_ch11.py -q
python tests/validate_course.py

写在最后

你已经实现标准 Bearer Token 认证闭环。下一篇不再增加接口功能,而是把测试提升为稳定工程体系:fixture、参数化、依赖替换和测试隔离。