当前位置: 首页 > news >正文

FastAPI JWT 登录认证实战:Access Token、Refresh Token 与权限保护

在前后端分离项目中,用户登录后如何保持身份状态,是后端开发必须解决的问题。

传统 Web 项目经常使用 Session:用户登录成功后,服务器保存会话数据,并向浏览器返回 Session ID。但在移动端、多服务部署和前后端分离场景中,JWT(JSON Web Token)是一种更常见的身份认证方案。

本文将使用 FastAPI 实现一套基础的 JWT 登录认证机制,内容包括:

  • 用户密码验证;

  • Access Token 的生成与解析;

  • Refresh Token 的设计;

  • 接口身份保护;

  • Token 过期与刷新;

  • 退出登录与 Token 撤销;

  • 实际项目中的安全注意事项。

一、JWT 是什么?

JWT 是一种可以在客户端和服务器之间传递声明信息的 Token 格式。

一个 JWT 通常由三部分组成:

Header.Payload.Signature

例如:

eyJhbGciOiJIUzI1NiJ9 . eyJzdWIiOiIxMDAxIiwiZXhwIjoxNzIwMDAwMDAwfQ . xxxxxxxxxxxxxxxx

三部分分别表示:

  • Header:使用的签名算法和 Token 类型;

  • Payload:用户 ID、过期时间、Token 类型等声明;

  • Signature:服务器根据密钥生成的签名。

JWT 的 Payload 只是经过 Base64URL 编码,并不是加密内容。因此,不应该在其中保存密码、身份证号、聊天内容等敏感信息。

JWT 签名的主要作用是防止内容被篡改,而不是隐藏内容。

二、JWT 登录认证的基本流程

一次完整的 JWT 登录过程如下:

用户提交账号和密码 ↓ 服务器验证用户信息 ↓ 生成 Access Token 和 Refresh Token ↓ 客户端保存 Token ↓ 请求接口时携带 Access Token ↓ 服务器验证签名和有效期 ↓ 允许或拒绝访问

客户端一般通过请求头携带 Token:

Authorization: Bearer <access_token>

Access Token 过期后,客户端可以使用 Refresh Token 获取新的 Access Token,而不需要用户立即重新输入密码。

三、安装项目依赖

安装 FastAPI、JWT 和密码哈希相关依赖:

pip install fastapi uvicorn pyjwt bcrypt

项目可以先使用一个文件演示:

jwt_demo/ └── main.py

启动命令:

uvicorn main:app --reload

四、不要明文保存用户密码

用户密码不能直接保存到数据库中,应该保存密码经过哈希计算后的结果。

可以使用bcrypt处理密码:

import bcrypt def hash_password(password: str) -> str: password_bytes = password.encode("utf-8") hashed = bcrypt.hashpw( password_bytes, bcrypt.gensalt(), ) return hashed.decode("utf-8") def verify_password( plain_password: str, hashed_password: str, ) -> bool: return bcrypt.checkpw( plain_password.encode("utf-8"), hashed_password.encode("utf-8"), )

注册用户时保存哈希结果:

password_hash = hash_password( "example_password" )

用户登录时,通过verify_password()判断输入密码是否正确。

密码哈希与普通加密不同。系统不需要还原用户的原始密码,只需要验证用户本次输入是否与之前保存的密码一致。

五、配置 JWT 密钥

JWT 签名密钥不能直接写死在代码仓库中,应该通过环境变量读取:

import os JWT_SECRET = os.environ["JWT_SECRET"] JWT_ALGORITHM = "HS256"

启动服务前配置环境变量:

export JWT_SECRET="请替换成足够长的随机字符串" uvicorn main:app --reload

生产环境中的密钥应该:

  • 具有足够的随机性;

  • 不提交到 Git 仓库;

  • 不直接输出到日志;

  • 定期进行安全检查;

  • 通过密钥管理服务或安全配置系统保存。

如果密钥泄露,攻击者就可能伪造合法 Token。

六、生成 Access Token

Access Token 用于访问需要登录的接口,其有效时间通常较短。

from datetime import ( datetime, timedelta, timezone, ) from uuid import uuid4 import jwt ACCESS_TOKEN_EXPIRE_MINUTES = 30 def create_access_token(user_id: int) -> str: now = datetime.now(timezone.utc) payload = { "sub": str(user_id), "type": "access", "iat": now, "exp": now + timedelta( minutes=ACCESS_TOKEN_EXPIRE_MINUTES ), "jti": str(uuid4()), } return jwt.encode( payload, JWT_SECRET, algorithm=JWT_ALGORITHM, )

这里使用了几个常见字段:

字段含义
subToken 对应的用户
typeToken 类型
iatToken 签发时间
expToken 过期时间
jtiToken 的唯一编号

建议使用 UTC 时间处理 Token 有效期,减少不同服务器时区产生的问题。

七、生成 Refresh Token

Refresh Token 只用于申请新的 Access Token,其有效期通常更长:

REFRESH_TOKEN_EXPIRE_DAYS = 7 def create_refresh_token(user_id: int) -> str: now = datetime.now(timezone.utc) payload = { "sub": str(user_id), "type": "refresh", "iat": now, "exp": now + timedelta( days=REFRESH_TOKEN_EXPIRE_DAYS ), "jti": str(uuid4()), } return jwt.encode( payload, JWT_SECRET, algorithm=JWT_ALGORITHM, )

虽然两种 Token 都使用 JWT 格式,但必须通过type字段进行区分。

如果后端没有检查 Token 类型,攻击者可能把有效期较长的 Refresh Token 当成 Access Token 使用,从而绕过原本的有效期设计。

八、实现用户登录接口

首先定义请求和响应模型:

from pydantic import BaseModel class LoginRequest(BaseModel): username: str password: str class TokenResponse(BaseModel): access_token: str refresh_token: str token_type: str = "bearer"

为了简化示例,使用字典模拟数据库:

fake_users = { "demo": { "id": 1001, "username": "demo", "password_hash": hash_password( "123456" ), "is_active": True, } }

实现登录接口:

from fastapi import FastAPI, HTTPException app = FastAPI() @app.post( "/auth/login", response_model=TokenResponse, ) def login(request: LoginRequest): user = fake_users.get(request.username) if not user: raise HTTPException( status_code=401, detail="用户名或密码错误", ) if not verify_password( request.password, user["password_hash"], ): raise HTTPException( status_code=401, detail="用户名或密码错误", ) if not user["is_active"]: raise HTTPException( status_code=403, detail="用户已被禁用", ) return TokenResponse( access_token=create_access_token( user["id"] ), refresh_token=create_refresh_token( user["id"] ), )

无论用户名不存在还是密码错误,接口都返回相同提示,可以避免向外部暴露账号是否存在。

九、解析和验证 Token

FastAPI 提供了OAuth2PasswordBearer,可以从请求头中提取 Bearer Token:

from fastapi.security import OAuth2PasswordBearer oauth2_scheme = OAuth2PasswordBearer( tokenUrl="/auth/login" )

然后实现当前用户解析逻辑:

from fastapi import Depends, HTTPException from jwt import ( ExpiredSignatureError, InvalidTokenError, ) def get_current_user_id( token: str = Depends(oauth2_scheme), ) -> int: try: payload = jwt.decode( token, JWT_SECRET, algorithms=[JWT_ALGORITHM], ) if payload.get("type") != "access": raise HTTPException( status_code=401, detail="Token 类型错误", ) user_id = payload.get("sub") if not user_id: raise HTTPException( status_code=401, detail="Token 缺少用户信息", ) return int(user_id) except ExpiredSignatureError: raise HTTPException( status_code=401, detail="Token 已过期", ) except ( InvalidTokenError, TypeError, ValueError, ): raise HTTPException( status_code=401, detail="无效的 Token", )

jwt.decode()会验证签名和过期时间。验证失败时,接口应该返回401 Unauthorized

十、保护需要登录的接口

有了get_current_user_id(),就可以通过依赖注入保护接口:

@app.get("/users/me") def get_current_user( user_id: int = Depends( get_current_user_id ), ): return { "user_id": user_id, "message": "身份验证成功", }

客户端请求时必须携带 Access Token:

curl \ -H "Authorization: Bearer <access_token>" \ http://localhost:8000/users/me

如果 Token 不存在、签名错误或已经过期,服务器会拒绝访问。

十一、实现 Token 刷新接口

定义刷新请求模型:

class RefreshRequest(BaseModel): refresh_token: str

刷新接口需要确认传入的是 Refresh Token:

@app.post("/auth/refresh") def refresh_access_token( request: RefreshRequest, ): try: payload = jwt.decode( request.refresh_token, JWT_SECRET, algorithms=[JWT_ALGORITHM], ) if payload.get("type") != "refresh": raise HTTPException( status_code=401, detail="Token 类型错误", ) user_id = payload.get("sub") if not user_id: raise HTTPException( status_code=401, detail="Token 缺少用户信息", ) return { "access_token": create_access_token( int(user_id) ), "token_type": "bearer", } except ExpiredSignatureError: raise HTTPException( status_code=401, detail="Refresh Token 已过期", ) except ( InvalidTokenError, TypeError, ValueError, ): raise HTTPException( status_code=401, detail="无效的 Refresh Token", )

实际项目中,刷新时还应该检查:

  • 用户是否仍然存在;

  • 用户是否已被禁用;

  • Refresh Token 是否被撤销;

  • 用户密码是否已经修改;

  • 当前设备是否仍然可信。

不能只验证 JWT 签名后就无条件签发新 Token。

十二、退出登录为什么不能只删除前端 Token?

JWT 的特点之一是服务器可以不保存会话状态。

这也意味着,只要 Token 没有过期,服务器通常就会认为它有效。用户在前端点击退出登录,只是删除了当前设备保存的 Token,并不能让已经泄露的 Token 立即失效。

如果业务要求退出后立即失效,可以使用 Redis 保存 Token 黑名单。

退出时,将 Access Token 的jti写入 Redis,并设置与 Token 剩余有效期相同的过期时间:

jwt:blacklist:<jti> = 1

验证 Token 时检查:

jti = payload.get("jti") if redis_client.exists( f"jwt:blacklist:{jti}" ): raise HTTPException( status_code=401, detail="Token 已失效", )

当 Token 自然过期后,对应的黑名单记录也可以自动删除。

另一种方案是保存用户的 Token 版本号。修改密码、退出所有设备或封禁账号时,提高版本号,让之前签发的 Token 全部失效。

十三、同言翻译场景中的身份认证设计

对于具有个人账号、历史会话和跨设备使用需求的应用,身份认证不仅关系到接口能否访问,也关系到用户数据是否会被错误读取。

同言翻译为例,用户可能需要查看自己的翻译记录、管理术语配置或在不同设备间同步会话。后端接口需要通过 Access Token 确认请求者身份,并在查询数据时同时校验资源归属关系。

下面这种写法只根据会话 ID 查询数据,存在越权风险:

@app.get("/sessions/{session_id}") def get_session(session_id: int): return query_session(session_id)

即使接口要求登录,用户仍然可能通过修改session_id访问其他人的会话。

更安全的方式是同时使用当前用户 ID 查询:

@app.get("/sessions/{session_id}") def get_session( session_id: int, user_id: int = Depends( get_current_user_id ), ): session = query_user_session( user_id=user_id, session_id=session_id, ) if not session: raise HTTPException( status_code=404, detail="会话不存在", ) return session

对于同言翻译这类涉及语音、文本和会话内容的应用,仅验证“用户是否登录”是不够的,还必须验证“当前用户是否有权访问这份数据”。

此外,可以为敏感操作增加更严格的安全措施,例如:

  • 修改密码前重新验证身份;

  • 导出历史记录时进行二次确认;

  • Refresh Token 按设备分别管理;

  • 异常登录后使旧 Token 失效;

  • 对重要接口记录安全审计日志;

  • 不在 JWT 中保存翻译原文或会话内容。

十四、Access Token 应该保存在哪里?

不同客户端需要采用不同的 Token 保存策略。

浏览器应用

常见方式包括:

  • 内存变量;

  • HttpOnly Cookie

  • sessionStorage

  • localStorage

将 Token 保存到localStorage实现简单,但如果页面存在 XSS 漏洞,恶意脚本可能读取 Token。

使用HttpOnly Cookie可以阻止 JavaScript 直接读取 Cookie,但需要额外处理 CSRF、防跨站请求和 Cookie 安全属性。

如果使用 Cookie,通常应该合理配置:

HttpOnly Secure SameSite

没有一种方案适合所有项目,需要结合前端架构、跨域方式和安全要求进行选择。

移动端应用

移动端应使用系统提供的安全存储能力,不建议把 Token 直接保存在普通配置文件或明文数据库中。

十五、生产环境中的安全建议

1. Access Token 不要设置得过长

Access Token 有效期越长,泄露后的风险持续时间越长。

可以使用:

短期 Access Token + 长期 Refresh Token

在用户体验和安全性之间取得平衡。

2. Refresh Token 应支持撤销

Refresh Token 的有效期较长,一旦泄露,攻击者可能不断申请新的 Access Token。

生产环境中可以在数据库或 Redis 中保存 Refresh Token 的状态,并记录:

  • Token 唯一编号;

  • 所属用户;

  • 登录设备;

  • 签发时间;

  • 过期时间;

  • 是否已经撤销。

3. 使用 HTTPS

如果使用明文 HTTP,Token 可能在传输过程中被截获。

生产环境必须使用 HTTPS,WebSocket 则应该使用wss://

4. 不要在日志中记录完整 Token

排查问题时,可以记录 Token 的jti、用户 ID或部分摘要,但不应该输出完整 Token。

5. 为登录接口增加限流

攻击者可能持续尝试不同密码,因此登录接口应该增加:

  • IP 限流;

  • 账号维度限流;

  • 连续失败次数限制;

  • 验证码或其他人机验证;

  • 异常登录告警。

6. 修改密码后撤销旧 Token

如果用户修改密码,但旧 Token 仍然可以继续使用,那么已经泄露的 Token 不会自动失效。

可以通过 Token 版本号、黑名单或会话记录实现统一撤销。

十六、JWT 常见误区

误区一:JWT 中的数据是加密的

JWT Payload 通常只是编码,任何获得 Token 的人都可以解析其中内容。

误区二:使用 JWT 就完全不需要服务器状态

如果需要退出登录、设备管理、Token 撤销和风险控制,服务器仍然可能需要保存部分状态。

误区三:Refresh Token 可以访问业务接口

Refresh Token 只能用于刷新身份凭证。业务接口必须检查 Token 类型,只接受 Access Token。

误区四:只要签名正确就代表用户可以访问所有数据

签名正确只能说明 Token 是服务器签发的。具体资源是否属于当前用户,仍然需要在业务层进行权限校验。

误区五:JWT 可以替代所有权限系统

JWT 负责传递身份信息,但角色权限、资源权限、数据归属和操作范围仍然需要单独设计。

十七、总结

使用 FastAPI 实现 JWT 登录认证,核心流程包括:

  1. 对用户密码进行安全哈希;

  2. 登录成功后生成 Access Token 和 Refresh Token;

  3. 客户端通过 Bearer Token 访问接口;

  4. 服务器验证签名、类型和过期时间;

  5. Access Token 过期后使用 Refresh Token 更新;

  6. 对重要接口进行资源归属和权限检查;

  7. 通过黑名单或 Token 版本实现主动撤销。

JWT 能够让前后端分离项目更方便地传递身份信息,但它并不是“生成一个字符串”这么简单。

真正可靠的认证系统,还需要综合考虑密钥管理、Token 存储、退出登录、设备管理、越权访问、接口限流和安全审计。

身份认证解决的是“你是谁”,权限校验解决的是“你能做什么”。只有同时做好这两部分,才能真正保护用户数据和系统接口。

http://www.jsqmd.com/news/1288270/

相关文章:

  • 英雄联盟Akari助手:3步实现智能游戏辅助的终极指南
  • 同样是多层板,少年派五优板差距居然这么大 - 博客湾
  • Windows PE文件逆向分析:从结构解析到恶意软件检测实战
  • 黄金关注缺口回补再多
  • 南京栖霞区马桶堵了怎么办?2026年7月找永昌管道疏通快速上门避坑指南 - 余生黄金回收
  • 单片机计算机毕设之 基于 STM32 的自动手动双模式酒精检测系统设计,基于嵌入式单片机的酒驾预检测报警终端开发(010201)
  • AI驱动的iPaaS系统集成厂商哪家好:国内企业AI化转型专业深度测评
  • 7A 160W双路直流电机驱动板:从原理到智能小车实战指南
  • AI编排框架对比:LangChain、LlamaIndex、Spring AI与Haystack的工程选型
  • 基于MicroPython与TFLite Micro的嵌入式人体识别实战指南
  • 【国家级AI治理工程师认证标准】:3类高危文档模板首次公开,仅限本周下载
  • GetQzonehistory:三步实现QQ空间历史说说完整备份的终极解决方案
  • Excel图文混排内容导入WANGEDITOR的技术实现
  • MacOS搭建Android开发环境全攻略:从零配置到高效开发
  • 单片机计算机毕设之基于 STM32 单片机的多按键环境设备调控系统设计,基于粉尘传感器的室内空气质量智能预警硬件开发(010301)
  • 2026雅思听力线上辅导课程专业测评:突破瓶颈、口碑优选 - 2027品牌AI展
  • 如何快速上手OpenArk:面向新手的完整Windows系统工具指南
  • Durand-Kerner算法详解:高效求解多项式全部根的数值方法
  • Windows图形渲染的革命:Mesa3D开源驱动生态深度解析
  • 大模型核心概念与Prompt设计实战指南
  • AI模型训练效果到底好不好?用这7个动态评估维度立刻诊断准确率陷阱
  • 拆解 AI 眼镜传感:MH231 镜腿检测 MH254 充电仓双检测
  • RPG Maker解密工具:三步解锁加密游戏资源的终极指南
  • 泉州护墙板制造厂哪家正规?本地业主避坑指南 - 资讯快报
  • BilibiliDown终极指南:5步轻松下载B站Hi-Res无损音频与高清视频
  • PIC18与A5000实现物联网安全连接的实战方案
  • 细胞迁移实验全解析:划痕与Transwell实验的设计、操作与避坑指南
  • Token大清算——当AI编程的账单开始逼近程序员的工资条
  • 全球仅17家头部AI Lab在用的提示词版本控制系统——支持分支比对、回滚审计、跨模型迁移的Git for Prompt 1.0正式解密
  • 江门厂房电缆拆除回收真实案例:3个客户如何省心变现 - 广东再生资源回收