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 的依赖注入统一保护接口。

相关推荐
ZhengEnCi6 小时前
模型训练完整流程:从数据准备到模型上线的 10 个关键步骤(全文可跑)
后端·算法
ragnwang6 小时前
Notes: Python pybind11 import C++ module
开发语言·c++·python
王中阳Go6 小时前
后端转 Agent 岗面试怎么答:8 个高频问题与应答口径
人工智能·后端·python·大语言模型
程序猿DD6 小时前
OctaFuse Gateway 2.18.0:MCP 接入支持 OAuth,供应商粘性绑定可视化管理
后端
weixin_307779137 小时前
RAG 面试陷阱——混合检索并非无条件优越:一个条件反转实验
人工智能·python·面试·职场和发展
云和数据.ChenGuang8 小时前
深入解析 Python 的 dir():从用法到 CPython 源码实现
开发语言·人工智能·python·langchain
ZhengEnCi8 小时前
Agent 规则管理:让 Memory 做索引、让 Skill 存规则——上下文上限实测与分工方案
后端
nice_lcj5208 小时前
深入理解 Java 反射:从 Class 对象到方法调用(含性能实测 + 迷你 IOC 实战 + 面试考点)
java·开发语言·后端·面试
骑着蜗牛撵大象3279 小时前
2026 后端 AI 工程化:Spring AI 管调用、MCP 管工具、Agent 内嵌管自治
java·人工智能·后端·spring·ai·可观测性·mcp
摸爬滚打的小李10 小时前
Spring XML + WAR + Tomcat 项目启动
后端