登录之后,后端如何在每次请求中确认用户身份?本文用 FastAPI 和 PyJWT,串起
登录 → 签发 Token → 携带 Token → 鉴权 → 访问接口。
1. JWT 的结构与认证机制
JWT(JSON Web Token)是一种用于传递身份信息的令牌格式 ,常用于登录认证。用户登录成功后,服务端会签发一个包含用户标识、过期时间等信息的 Token,并通过签名防止内容被篡改。客户端在后续请求中携带这个 Token,服务端校验签名和有效期,再结合账号状态确认用户身份。它的主要作用是:让用户登录后,通过 Token 访问需要认证的接口,无需每次都提交账号密码。 JWT 标准
本例使用签名 JWT(JWS),结构为 Header.Payload.Signature:
| 组成 | 作用 |
|---|---|
| Header,头部 | 声明签名算法等元信息 |
| Payload,载荷 | 保存声明,如 user_id 和 exp |
| Signature,签名 | 用密钥校验内容是否被篡改 |
签名用于防篡改,载荷仍可解码读取。不要放入密码等敏感信息。 结构说明
2. 密钥管理与配置加载
本例采用 HS256(HMAC-SHA256) ,签发与验签共用服务端密钥。持有密钥的一方也能签发 Token,因此密钥只交给可信服务。密钥至少为 256 位(32 字节),并应随机生成。 算法标准
使用 JWT Secret Key Generator:在 Standard Secret Key 区域选择 256 bits ,点击 Generate Key → Copy to Clipboard ,填入 env/.env.dev:
dotenv
JWT_SECRET_KEY=替换为生成的随机密钥
JWT_EXPIRE_HOURS=24
JWT_ALGORITHM=HS256
配置优先级为 构造参数 → 系统环境变量 → 环境文件 → 默认值 ,前者覆盖后者。配置文档
默认读取 env/.env.dev;启动前设置 ENVIRONMENT=prod,则读取 env/.env.prod。JWT_EXPIRE_HOURS=24 表示有效期为 24 小时。
默认密钥是占位值,必须替换为随机密钥。修改环境配置后,需要重启应用。
3. Token 的签发与校验
签发时保存用户编号与到期时间;校验时验证签名、有效期和必要字段。
app/utils/jwt.py:
python
from datetime import UTC, datetime, timedelta
from typing import Any
import jwt
from app.core.config import settings
def create_access_token(user_id: int) -> str:
"""按 UTC 时间生成带用户 ID 和过期时间的 Token。"""
expire = datetime.now(UTC) + timedelta(hours=settings.JWT_EXPIRE_HOURS)
payload = {"user_id": user_id, "exp": expire}
return jwt.encode(payload, settings.JWT_SECRET_KEY, algorithm=settings.JWT_ALGORITHM)
def decode_access_token(token: str) -> dict[str, Any]:
"""校验签名和过期时间,要求 Token 包含 exp 与 user_id。"""
return jwt.decode(
token,
settings.JWT_SECRET_KEY,
algorithms=[settings.JWT_ALGORITHM],
options={"require": ["exp", "user_id"]},
)
PyJWT 会将 UTC 日期转为时间戳;Token 过期时抛出 ExpiredSignatureError。有效期校验
允许的算法由服务端固定。 require 只保证字段存在,user_id 的取值还要在鉴权时检查。PyJWT API
4. 登录流程与凭证返回
登录流程:查询用户 → 验证密码哈希 → 检查账号状态 → 签发 Token。
app/modules/users/crud.py:
python
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.common.response import Response
from app.modules.users.models import User
from app.modules.users.schema import LoginRequest, LoginResponse, LoginResult
from app.utils.jwt import create_access_token
from app.utils.password import verify_password
async def login_db(db: AsyncSession, data: LoginRequest) -> Response[LoginResult]:
statement = select(User).where(User.username == data.username)
result = await db.execute(statement)
user = result.scalar_one_or_none()
# 验证密码是否匹配
if not user or not verify_password(data.password, user.password):
return Response.error(401, "用户不存在或密码错误")
# 验证用户状态
if user.status != 1:
return Response.error(403, "用户状态异常")
# 创建token
token = create_access_token(user.id)
return Response.success(
message="登录成功",
data=LoginResult(token=token, user=LoginResponse.model_validate(user)),
)
成功后,客户端保存 data.token ,用于后续请求;LoginResponse 只返回公开用户字段,避免泄露密码哈希。
5. 请求鉴权与受保护接口
请求头统一使用 Authorization: Bearer <token> 。持有有效 Token 就能使用它,生产环境应通过 HTTPS 传输。Bearer 规范
鉴权依次检查 Token、用户编号与当前账号状态。
app/utils/auth.py:
python
from typing import Annotated
from fastapi import Security
from fastapi.security import APIKeyHeader
from jwt import ExpiredSignatureError, InvalidTokenError
from app.common.response import Response
from app.core.database import DatabaseSession
from app.modules.users.models import User
from app.utils.jwt import decode_access_token
authorization_header = APIKeyHeader(
name="Authorization",
scheme_name="BearerAuth",
description="填写 `Bearer <token>`,也兼容直接填写登录接口返回的 token。",
auto_error=False,
)
async def get_current_user(
authorization: Annotated[str | None, Security(authorization_header)],
db: DatabaseSession,
) -> User:
"""校验 Token,并读取仍然有效的当前用户。"""
if not authorization or not authorization.strip():
return Response.error(401, "请先登录")
# 兼容 Bearer Token 与直接传 Token,拒绝其他认证格式。
parts = authorization.split()
if len(parts) == 1:
token = parts[0]
elif len(parts) == 2 and parts[0].lower() == "bearer":
token = parts[1]
else:
return Response.error(401, "无效的 token")
try:
payload = decode_access_token(token)
except ExpiredSignatureError:
return Response.error(401, "登录已过期,请重新登录")
except InvalidTokenError:
return Response.error(401, "无效的 token")
user_id = payload.get("user_id")
if isinstance(user_id, bool) or not isinstance(user_id, int) or user_id <= 0:
return Response.error(401, "无效的 token")
# 每次查询账户状态,让删除或禁用用户立即生效。
user = await db.get(User, user_id)
if user is None:
return Response.error(401, "用户不存在")
if user.status != 1:
return Response.error(403, "用户状态异常")
return user
每次查询当前账号状态 ,让删除或禁用用户在后续请求中生效。Response.error() 内部抛出异常并中断流程,随后由统一异常处理器生成响应。
接口通过 Depends(get_current_user) 先完成鉴权,再获得当前用户:
app/modules/users/controller.py:
python
from typing import Annotated
from fastapi import APIRouter, Depends
from app.common.response import Response
from app.core.database import DatabaseSession
from app.modules.users.crud import login_db
from app.modules.users.models import User
from app.modules.users.schema import LoginRequest, LoginResponse, LoginResult
from app.utils.auth import get_current_user
router = APIRouter(prefix="/users", tags=["用户"])
@router.post("/login", summary="用户登录", response_model=Response[LoginResult])
async def user_login_api(data: LoginRequest, db: DatabaseSession):
return await login_db(db, data)
@router.get("/info", summary="获取用户信息", response_model=Response[LoginResponse])
async def get_userinfo_api(
current_user: Annotated[User, Depends(get_current_user)],
):
# response_model 负责校验并过滤 ORM 字段,避免返回密码。
return Response.success(data=current_user)
| 请求情况 | HTTP 状态 |
|---|---|
| Token 有效,账号正常 | 200,允许访问 |
| Token 缺失、无效、过期,或用户已删除 | 401,缺少有效认证凭证 |
| Token 有效,账号已禁用 | 403,当前账号不允许访问 |
6. 总结
JWT 登录认证的核心流程是 验证账号密码 → 签发 Token → 请求携带 Token → 验证凭证与账号状态 → 执行业务接口。
本例通过配置管理密钥,用 PyJWT 完成签发与校验,再用 FastAPI 的依赖注入统一保护接口。