FastAPI中jwt的基本使用与实践

登录之后,后端如何在每次请求中确认用户身份?本文用 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 的依赖注入统一保护接口。

相关推荐
mklpo1471 小时前
为不支持sse4.2命令集的老旧设备制作Win11 26H2系统镜像,2026-10-11。
运维·windows·python
GoldenSpider.AI1 小时前
用 AI 改流程之前先做「四桶」体检:9 次返工里 8 次靠规则就能消掉(附 Python 发布前校验器)
人工智能·python·自动化·ai智能体
FullStackGeek1 小时前
微信缴费APIV3接入
后端
小白快快跑哦1 小时前
python-字符串全解(六):正则表达式-修饰符
python·正则表达式·修饰符
灯澜忆梦1 小时前
【Go Zap 日志库】#1 |快速上手 + 配置详解
开发语言·后端·golang
m0_587383002 小时前
无人自助拼豆系统从需求分析到落地部署全流程实战开发指南
java·经验分享·后端·struts·小程序·架构·需求分析
Rain的Java大神之路2 小时前
被问麻了!Function Calling、MCP、Skill 到底啥关系?一张图讲透三层架构,附 Java 生产落地全流程+30个坑
java·后端·ai编程
niucloud-admin2 小时前
JAVA V6 多商户商城 开发文档——项目启动
java·开发语言·python
CCIE-Yasuo2 小时前
个人电脑搭建AI-Qwen3-8B-学习记录
人工智能·python·学习