你的APP要用户反复登录?密码传来传去?FastAPI+JWT实战,一个令牌全打通,安全与体验兼得,代码直接抄

43 阅读8分钟

JWT123asasd

深夜,你又收到一条用户投诉:“我在你们App上刚买完东西,跳到客服页面为什么又要我登录一次?!”

你盯着屏幕,赶紧回复 “抱歉” 。

心里一阵苦笑:还不是因为那两个老系统互不认账,用户状态根本同步不了。更让你心虚的是,有些接口还有用基础认证,密码在请求里 “裸奔”……

这不是你一个人的问题。当应用拆分成多个服务,“如何让用户一路畅通,又能保证安全?” 成了系统架构中最磨人的痛点之一。

今天,我就带你用 FastAPI + JWT,打造一把“万能密钥”。从此,用户一次登录,一路通行

为什么我的 API 需要 JWT?

试想一下,用户在你的App上刚登录完,当跳转到后台管理页面,又要输一次密码……。这种体验,用户会不会扭头就走?

这就是传统身份验证的痛点

  1. 状态难以跨系统维持;
  2. 敏感信息(如密码)反复传输
  3. 安全性与便利性难以兼得。

JWT 的救赎之道

而 JWT 的出现,正是为了解决这一核心冲突。它就像一张数字通行证,用户只需登录一次,即可在多个关联服务间畅通无阻,系统既能识别用户身份,又无需保存会话状态。

JWT 究竟是什么?它真的安全吗?

参考:www.jwt.io/introductio…

JWT 究竟是什么?

JWT(JSON Web Token)就是用户登录成功后,后端返回的 “加密电子身份证”—— 前端存起来,后续请求核心接口时带上,后端校验 token 有效就放行。

JWT 的三段式结构

先来看一个 JWT 令牌示例:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30

这个 JWT 令牌看着像一串天书,它实际上由三部分组成,用点(.)分隔:

{Header}.{Payload}.{Signature}

Header(头部):指明令牌类型和签名算法。

{
  "alg": "HS256",
  "typ": "JWT"
}

然后,这个 JSON 经Base64编码为 Base64Url,形成JWT的第一部分。

Payload(载荷):包含声明数据。声明有三种类型:注册声明、公共声明、私有声明。

注册声明:预定义字段,非强制但推荐。如iss(签发者)、exp(过期时间)

公共声明:自定义字段,需避免冲突

私有声明:双方约定的自定义字段

示例:

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

Signature(签名):签名是JWT的安全核心。

使用 HMAC SHA256 算法,签名方式如下:

# 伪代码演示签名过程
signature = HMACSHA256(
    base64UrlEncode(header) + "." +
    base64UrlEncode(payload),
    secret
)

签名确保令牌未被篡改,因为只有服务器拥有密钥,客户端无法伪造有效签名。

JWT 的工作流程:一次登录,处处认证
  1. 登录:用户提供用户名密码,服务器验证通过后,生成一个包含用户身份(如sub: "user_id")和过期时间的 JWT,将其返回给客户端(通常放在Authorization: Bearer <token>头中)。
  2. 访问资源:客户端在后续请求中携带此 JWT。
  3. 验证权限:服务器验证JWT签名和有效期,从Payload中提取用户信息,即可完成身份认证和授权,无需查询数据库会话。

实战:基于 FastAPI 的 JWT 完整实现

01、安装依赖与配置密钥

安装依赖包

pip install "python-jose[cryptography]" -i https://pypi.tuna.tsinghua.edu.cn/simple

生成一个高强度的密钥(切勿使用简单字符串):

(.venv) wangerge_notes: TodoApp$ openssl rand -hex 32
eec7f9b96330e64eaac29ad7c95154cff3addaea90239c262b3f9287e678f6d3

在配置文件中设置:

# auth.py
SECRET_KEY = "eec7f9b96330e64eaac29ad7c95154cff3addaea90239c262b3f9287e678f6d3"
ALGORITHM = "HS256"
02、核心方法——生成JWT令牌
# auth.py

def create_access_token(
    username: str, user_id: int, expires_delta: timedelta):
    # 载荷:仅存非敏感信息
    encode = {"sub": username, "id": user_id}
    # 过期时间(修正:时区正确,避免过期异常)
    expire = datetime.now(timezone.utc) + expires_delta
    encode.update({"exp": expire})
    # 编码生成token
    return jwt.encode(encode, SECRET_KEY, algorithm=ALGORITHM)
03、创建登录接口,返回令牌
# 定义响应模型(确保返回格式统一,避免验证错误)
class Token(BaseModel):
    access_token: str
    token_type: str


# 验证用户
def authenticate_user(username: str, password: str, db):
    user = db.query(Users).filter(Users.username == username).first()
    if not user:
        return False
    if not bcrypt_context.verify(password, user.hashed_password):
        return False
    return user

        
@router.post("/token", response_model=Token)
async def login_for_access_token(
    form_data: Annotated[OAuth2PasswordRequestForm, Depends()],
    db: Annotated[Session, Depends(get_db)]
):
    # 校验用户名和密码
    user = authenticate_user(form_data.username, form_data.password, db)
    # 检验失败,抛出异常
    if not user:
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED,
                            detail="Could not validate user.")
    # 生成token
    token = create_access_token(user.username, user.id, timedelta(minutes=20))
    return {"access_token": access_token, "token_type": "bearer"}
04、登录接口测试

切回浏览器 Swagger UI 页面,刷新页面,找到 POST /token 接口,给传入 username=wangerge, password=test1234。这个用户名和密码是数据库里面的。

执行请求,返回响应码 200,响应体如下:

{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ3YW5nZXJnZSIsImlkIjoxLCJleHAiOjE3NjgxMzI0NTJ9.eiUqiVl8GHQx_bG61UvAzvAuuODInk5Szz5yPT5zkZs",
  "token_type": "bearer"
}

此时,客户端传入的用户名和密码验证通过,返回加密 token。

再次切回浏览器 Swagger UI 页面,刷新页面,找到 POST /token 接口,给传入 username=wangerge, password=test。(传入一个错误的密码)

执行请求,返回响应码 401,响应体如下:

{
  "detail": "Could not validate user."
}

此时,客户端传入的用户名和密码验证失败,返回异常信息。

至此,当客户端通过浏览器给服务器传入一个正确的用户名和密码后,服务器能正确的生成 JWT 令牌信息,并返回给客户端。

05、JWT 令牌验证,保护授权接口

添加函数 get_current_user,解码并验证 JWT 令牌。

from datetime import timedelta, datetime, timezone
from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException
from fastapi.security import OAuth2PasswordBearer
from jose import jwt, JWTError

auth2_bearer = OAuth2PasswordBearer(tokenUrl="token")


async def get_current_user(
    token: Annotated[str, Depends(auth2_bearer)]):

    try:
        # 检查令牌的真实性。
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username: str = payload.get("sub")
        user_id: int = payload.get("id")
        if username is None or user_id is None:
            raise HTTPException(
                status_code=status.HTTP_401_UNAUTHORIZED,
                detail="Could not validate user.")
        return {"username": username, "id": user_id}
    
    except JWTError:
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED,
                            detail="Could not validate user.")

在上面代码中,我们解码 token ,得出用户名和用户 ID,并结构化返回。

下面给授权接口添加用户验证。

来到 todos.py 文件,导入 get_current_user 函数

from .auth import get_current_user

添加 user_dependency 依赖注入

user_dependency = Annotated[dict, Depends(get_current_user)]

来到 GET /todo/ 接口处,现在,要求这个接口必须要获取到用户登录信息后,才可以正常请求。

read_all 方法,添加 user: user_dependency 参数,并添加 user 判断。代码如下:

@router.get("/todo")
async def read_all(user: user_dependency, db: db_dependency):
    if not user:
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED,
                            detail="Failed Authentication")

    return db.query(models.Todos).all()

切回浏览器 Swagger UI 页面,刷新页面,找到 GET /todo/ 接口,执行请求。

返回响应码 401,响应体:"detail": "Not authenticated"

这表示需要一个用户认证,才能执行 API 请求,这就是用户验证。

我们找到 Swagger UI 页面 POST /todo 接口,在列表栏最右边,可以找到一个小锁的图标。如下图:

image-20260113150121905

点击打开它,提示我们需要输入用户名和密码。

输入 username="wangerge" password="test1234" 点击 “Authorize” 按钮提交。

认证成功,结果如下图:

image-20260113150514048

现在,用户已经登录成功了。重新执行 GET /todo/ 接口请求。

服务器返回响应码 200。接口请求成功了。

JWT 带来的变革

通过以上实验,我们解决了开篇提到的痛点:

  1. 单点登录(SSO)体验:用户登录一次,令牌在多服务间通用。
  2. 无状态架构:服务器无需保存会话,轻松扩展,完美契合微服务。
  3. 安全可控:基于签名的防篡改、可设置过期时间、Payload 携带基础信息。

避坑指南:

  1. 保管好你的SECRET_KEY:它是安全的基石。
  2. 合理设置令牌过期时间:根据场景合理设置,平衡安全与体验。
  3. 敏感数据不上令牌:JWT 的 Payload 是可解码的。

JWT 在 API 身份验证与授权的道路上,无疑是一把锋利而优雅的瑞士军刀。

想要获取本章完整代码,请在评论区回复 【FastAPI】,代码直接复制就能跑。

关于 FastAPI 的其他疑问

FastAPI极速上手:从API到全栈,高薪后端必备技能

10分钟用Python搭个接口,还能自动生成文档?

3分钟搞定Python虚拟环境和FastAPI安装

FastAPI实战:3步搞定增删改查,你的第一个完整API来了!

别再堆if-else验参数了!FastAPI自带的参数验证器,至少省一半调试时间

3分钟搞定FastAPI的数据库设置,把数据存储玩明白,复制代码就能用

10分钟搞定FastAPI中“数据库连接管理、参数校验、文档维护”三大核心难题,让新手也能轻松地写出可落地的API

“警惕!FastAPI接口一夜「消失」” 95%程序员靠这招自救:我的路由分离血泪史

相关内容我都给大家做好了,感兴趣的朋友来「我的主页」找一找,直接就可以看到。

关注我,每天分享「Python」、「职场」有趣干货,千万不要错过!