Skip to content

登录认证、密码与 Token ​

认证和授权 ​

认证回答“你是谁”;授权回答“你能做什么”。用户登录成功,不代表可以读取所有人的数据或执行管理员操作。

基本流程:

text
提交用户名与密码
    ↓
服务器验证密码
    ↓
签发有时效的访问 Token
    ↓
客户端请求时携带 Authorization: Bearer <token>
    ↓
服务器校验 Token,再查询用户与权限
    ↓
允许或拒绝本次操作

密码不能直接存明文 ​

密码哈希把密码处理成适合验证的结果,登录时比较输入密码与已保存哈希是否匹配。

应使用专门的密码哈希库与合适参数,而不是自己用一次 MD5 或 SHA256 代替。密码哈希通常包含随机盐,所以同一密码多次生成的哈希可以不同,但仍能正确验证。

本章使用 pwdlib 的推荐配置和 Argon2 支持;JWT 使用 PyJWT。这也对应当前 FastAPI 官方安全教程采用的工具。官方密码哈希与 JWT 教程

JWT:有签名的声明数据 ​

JWT 常带有用户标识 sub 和过期时间 exp。服务器验证签名和过期条件,判断这份声明是否可信。

签名用于发现篡改,普通签名 JWT 的载荷不是保密的。不要在其中放密码、私密资料或数据库密钥。读取载荷不等于校验通过;必须验证签名、算法和所需声明。

安装与准备密钥 ​

powershell
python -m pip install PyJWT "pwdlib[argon2]" pydantic-settings
$env:JWT_SECRET = python -c "import secrets; print(secrets.token_hex(32))"

这个变量只设置在当前 PowerShell 会话中。随后在同一窗口启动服务。该密钥用于学习环境;需要稳定验证既有 Token 时,应妥善保存并保持一致。每次重新生成密钥,旧 Token 就无法通过原算法的签名验证。

完整示例:auth_demo.py ​

为了独立练习,这里用内存模拟一个用户,不实现注册或数据库存储。教学账号是 learner,密码是 LearnFastAPI123!;不要把这类公开固定账号用在真实应用中。

python
from datetime import datetime, timedelta, timezone
from typing import Annotated

import jwt
from jwt.exceptions import InvalidTokenError
from fastapi import Depends, FastAPI, HTTPException
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from pydantic import BaseModel, Field
from pydantic_settings import BaseSettings
from pwdlib import PasswordHash


class Settings(BaseSettings):
    jwt_secret: str = Field(min_length=32)


settings = Settings()
algorithm = "HS256"
password_hash = PasswordHash.recommended()
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
app = FastAPI(title="认证学习示例")


class UserPublic(BaseModel):
    username: str
    is_admin: bool = False


class UserInDB(UserPublic):
    password_hash: str


class Token(BaseModel):
    access_token: str
    token_type: str = "bearer"


users = {
    "learner": UserInDB(
        username="learner",
        password_hash=password_hash.hash("LearnFastAPI123!"),
        is_admin=False,
    )
}
dummy_hash = password_hash.hash("UnusedDummyPassword123!")


def credentials_error() -> HTTPException:
    return HTTPException(
        status_code=401,
        detail="用户名、密码或访问凭据无效",
        headers={"WWW-Authenticate": "Bearer"},
    )


def create_access_token(username: str) -> str:
    expires = datetime.now(timezone.utc) + timedelta(minutes=30)
    payload = {"sub": username, "exp": expires}
    return jwt.encode(payload, settings.jwt_secret, algorithm=algorithm)


@app.post("/token", response_model=Token)
def login(form: Annotated[OAuth2PasswordRequestForm, Depends()]):
    user = users.get(form.username)
    stored_hash = user.password_hash if user else dummy_hash
    valid_password = password_hash.verify(form.password, stored_hash)
    if user is None or not valid_password:
        raise credentials_error()
    return Token(access_token=create_access_token(user.username))


def get_current_user(
    token: Annotated[str, Depends(oauth2_scheme)],
) -> UserInDB:
    try:
        payload = jwt.decode(
            token,
            settings.jwt_secret,
            algorithms=[algorithm],
            options={"require": ["exp", "sub"]},
        )
        username = payload.get("sub")
        if not isinstance(username, str) or not username:
            raise credentials_error()
    except InvalidTokenError:
        raise credentials_error()

    user = users.get(username)
    if user is None:
        raise credentials_error()
    return user


CurrentUser = Annotated[UserInDB, Depends(get_current_user)]


@app.get("/users/me", response_model=UserPublic)
def read_me(user: CurrentUser):
    return user


def require_admin(user: CurrentUser) -> UserInDB:
    if not user.is_admin:
        raise HTTPException(status_code=403, detail="需要管理员权限")
    return user


@app.get("/admin", dependencies=[Depends(require_admin)])
def admin_area():
    return {"message": "管理员区域"}

启动:

powershell
python -m uvicorn auth_demo:app --reload

OAuth2PasswordBearer 负责读取 Bearer Token 并描述安全方案,它不会替你检查签名,也不会自动创建 /token 接口。签名校验和用户查询由 get_current_user 完成。官方安全流程起步说明、PyJWT 编解码说明

在 /docs 中验证 ​

  1. 不登录调用 /users/me,应返回 401。
  2. 展开 POST /token,按表单提交教学用户名与密码。
  3. 观察 access_token 和 token_type。
  4. 点击文档中的 Authorize,按界面填写用户名和密码完成授权。
  5. 再请求 /users/me,应得到用户名与管理员状态,不包含密码哈希。
  6. 请求 /admin,由于教学用户不是管理员,应得到 403。
  7. 修改 Token 中任意字符,再发送请求,应该校验失败。

OAuth2PasswordRequestForm 接收的是表单,不是 JSON。Python 客户端测试时使用 data={"username": ..., "password": ...},而不是 json=...。

从演示到真实项目还需要什么 ​

项目能力为什么需要
用户数据库保存真实用户、哈希和用户状态
对象归属检查任务只能由所有者或获授权的人访问
HTTPS保护密码和 Token 的传输
登录限流与审计处理频繁失败、排查异常
Token 生命周期策略确定过期、刷新、注销与撤销方案
密钥管理保持一致、限制访问、支持轮换

JWT 不会自动支持即时注销。如何在到期前撤销访问,可能需要短时 Token、服务端会话或撤销记录等设计。若接入统一身份平台,按平台要求选择适合的授权流程,不要直接把教学登录例子当成完整身份系统。

本章练习 ​

给任务表增加 owner_id,设计“只查询当前用户任务”的依赖和数据库条件。不要只在前端隐藏其他用户的任务。

过关标准:能区分密码哈希、Token、JWT 签名、身份认证和权限检查,并解释 401 与 403。