恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

FastAPI + JWT 实战:无状态身份认证与安全登录全解析

  • 首页
  • 资讯中心
  • /
  • FastAPI + JWT 实战:无状态身份认证与安全登录全解析

相关资讯

储能AGC调频仿真怎么做?基于Simulink的控制策略与参数整定实践 2026/10/9 8:43:29
GPU算力服务器配置与机器学习框架优化:从训练加速到推理部署全指南 2026/10/9 8:38:28
制造业研发管理数字化:本土化方案为何更贴合实际流程与协作 2026/10/9 8:38:28

最新资讯

代码评审记录表:让评审从口头聊天变成工程资产
VCS用户指南高效使用:从编译参数到覆盖率调试的完整指南
ProfiNet转EtherCAT网关选型与配置:2026年天津定制化厂家实战指南
Claude Code 添加 MCP 服务器完整指南:把 settings 改到 TaoToken
VS code中一键对齐符号的插件配置指南【超好用】
AI 客服本地部署和云端部署怎么选?数据、成本、维护三笔账

今日推荐

AI编程智能体实战:从写代码到指挥代码的架构与落地
多模态大模型全栈能力拆解:从数据对齐到弹性推理
大模型Agent开发入门:从工具调用循环到落地避坑指南

本周热门

MR25H40CDF + PIC18F65K40:工业记录仪高可靠存储实战
基于STM32的数控恒压恒流电源设计:从硬件到PID调参全解析
LT9211 MIPI重定时器原理与双路扇出实战指南

本月精选

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)

FastAPI + JWT 实战:无状态身份认证与安全登录全解析

发布时间:2026/10/9 8:43:29
FastAPI + JWT 实战:无状态身份认证与安全登录全解析 登录这件事几乎每个做Web应用的人都被折磨过。用户明明输入了正确的密码却因为Session过期被一脚踢回登录页后端为了校验身份每次请求都要去数据库里捞一遍Session记录再做单体应用还好一旦拆成微服务Session同步就成了噩梦。你让用户反复登录用户就会反复卸载你的APP。让密码在各系统之间传来传去更是把安全底线踩在脚下。我自己的解决思路很简单用FastAPI做后端用JWT做无状态令牌一次登录、到处通行安全性与用户体验可以同时握住。这篇文章就把我实际的落地过程完整写出来代码、配置、排查思路都给你照着抄就能跑。这篇内容适合三类人看一是刚接触FastAPI、想找一套正经鉴权方案的人二是被Session共享、分布式登录搞得头大的后端开发三是写SPA或小程序前端、想知道令牌到底怎么用才安全的人。全套代码我尽量贴全但重点讲清楚背后的设计逻辑。1. 为什么是JWT而不是Session一轮简单的账先说个典型场景。以前用Flask或Django写登录很常见的做法是服务端生成一个随机的Session ID塞进用户的Cookie里然后把用户数据存到服务端的内存或Redis里。用户下次带着Cookie来服务端去Redis里查一下查到就算登录过。这套方案在小项目里完全没问题但它有个致命的天然缺陷服务端是有状态的。只要用户量一上来、服务一多问题就接踵而至。比如你有三个后端实例用户第一次请求打到了A机器Session存在A的内存里第二次请求被负载均衡转发到了B机器B的内存里根本没有这个Session用户就被当成未登录了。解决办法也简单粗暴要么做Session粘滞要么搞一台统一的Redis专门存Session但这两样都让架构变重、变复杂。JWT走的是完全相反的思路。服务端不存任何东西登录成功后把用户ID、过期时间、自定义字段打包进一个JSON结构用密钥做签名然后把这个字符串发给前端。前端每次请求带回来服务端只需要验一下签、查一下过期时间就算完成了身份确认。服务端彻底无状态任何一个节点都能独立完成鉴权水平扩展时几乎没有额外成本。当然JWT也不是银弹。它最常被人诟病的点有两个一是“无法主动失效”用户的令牌一旦签发在到期之前你很难强制让它作废二是令牌里如果塞了太多东西体积会膨胀每次请求都要带上。这两个问题后面我会专门讲解法不是无解的。再看Flask和FastAPI的对比。Flask作为老牌框架生态成熟、上手简单但它是同步框架在高并发IO场景下需要自己折腾异步支持FastAPI从设计之初就是异步的性能表现更接近Node.js加上自动生成OpenAPI文档、基于类型注解的参数校验在写API这类场景里效率高出不少。既然要做纯后端API、又要考虑性能和开发体验FastAPI在这个项目里就是更顺手的那个。2. 项目目录设计与依赖准备动手之前先搭架子很多新手写FastAPI项目上来就把所有代码塞进一个main.py里路由、模型、鉴权逻辑全挤在一起。一个登录功能还好加上用户管理、订单、文章几百行代码堆在同一个文件里别说维护看一眼都头疼。我常用的目录结构是这样划分的app/ ├── main.py # 应用入口创建FastAPI实例、注册路由 ├── config.py # 配置管理读取密钥、过期时间等 ├── database.py # 数据库连接与Session管理 ├── models.py # SQLAlchemy ORM模型 ├── schemas.py # Pydantic模型做参数校验与序列化 ├── security.py # 密码哈希、JWT生成与校验 ├── dependencies.py # FastAPI依赖用于注入当前用户 └── routers/ ├── __init__.py ├── auth.py # 登录、注册、刷新令牌的接口 ├── users.py # 用户信息相关接口 └── ...这样划分的原因很直接每个文件职责单一改鉴权逻辑不用动路由文件加接口不用动模型文件多人协作时Git冲突也少。尤其是security.py和dependencies.py这两个文件它们是整个鉴权体系的核心拆出来单独管理后续做权限扩展、加刷新令牌、加黑名单都容易。依赖方面我用的核心库不多最基础就这么几个pip install fastapi uvicorn[standard] sqlalchemy pydantic pyjwt passlib[bcrypt] python-multipart几个关键选型说下理由。数据库操作用SQLAlchemy因为它是ORM里最稳的配合PostgreSQL、MySQL、SQLite都能用调试方便密码哈希用passlib加bcryptbcrypt算法自带盐、且计算速度适中能有效对抗暴力破解JWT库用PyJWT它轻量、文档全是最常见的Python JWT实现。主要依赖装完之后我还额外装了python-dotenv用来管理环境变量pip install python-dotenv.env文件里放的东西要拎清楚JWT密钥、数据库连接串、令牌过期时间这些都算敏感配置不能写死到代码里更不能上传到Git仓库。# .env SECRET_KEYyour-super-secret-key-change-me ACCESS_TOKEN_EXPIRE_MINUTES30 REFRESH_TOKEN_EXPIRE_DAYS7 DATABASE_URLsqlite:///./test.db3. 核心代码这样写从密码存储到令牌签发全链路3.1 密码哈希务必自己加盐用户密码是绝对不可以明文存到数据库的这一点应该没有争议。我见过一些教程直接User表里存Password字段对比时明文比对这种代码只要数据库泄露所有账号立刻裸奔。用passlib来做密码哈希和管理代码非常简洁from passlib.context import CryptContext pwd_context CryptContext(schemes[bcrypt], deprecatedauto) def hash_password(password: str) - str: return pwd_context.hash(password) def verify_password(plain_password: str, hashed_password: str) - bool: return pwd_context.verify(plain_password, hashed_password)这里有个细节值得注意passlib的CryptContext会自动生成并附加盐值不需要你自己拼盐也不要手动往密码里加固定的盐。固定盐意味着所有用户在哈希时的“扰动”都一样一旦盐泄露攻击者可以批量制作彩虹表。bcrypt的自动加盐每个用户都不同才是真正安全的做法。3.2 登录接口更新用户登录信息并返回令牌用户提供用户名和密码之后后端要做的其实有三件事验证身份、更新最后登录时间、签发令牌并返回。很多代码只做第一件和第三件把“记录登录时间”这种审计信息漏掉了出问题想排查时连个时间线都拉不出来。我习惯把这三件事放在一个接口里做完from datetime import datetime, timedelta from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.orm import Session import jwt from .. import schemas, models, security from ..database import get_db router APIRouter(prefix/auth, tags[认证]) router.post(/login, response_modelschemas.TokenResponse) def login( credentials: schemas.LoginRequest, db: Session Depends(get_db) ): user db.query(models.User).filter( models.User.username credentials.username ).first() if not user or not security.verify_password(credentials.password, user.hashed_password): raise HTTPException( status_codestatus.HTTP_401_UNAUTHORIZED, detail用户名或密码错误 ) # 1. 更新登录时间 user.last_login_at datetime.utcnow() db.add(user) db.commit() # 2. 生成令牌 access_token security.create_access_token({sub: str(user.id)}) refresh_token security.create_refresh_token({sub: str(user.id)}) # 3. 返回完整登录信息 return schemas.TokenResponse( access_tokenaccess_token, refresh_tokenrefresh_token, token_typebearer, expires_insecurity.ACCESS_TOKEN_EXPIRE_MINUTES * 60 )为什么单独定义get_db依赖而不是在接口里直接操作数据库连接因为FastAPI的依赖注入机制能保证每个请求函数执行完后自动关闭数据库Session不会出现连接泄漏。这一点在长运行的服务里尤其重要连接池被占满比接口变慢更难排查。3.3 令牌生成Payload里该放什么、不该放什么JWT分为三部分Header、Payload、Signature。Header声明算法和令牌类型Payload放实际数据Signature是签名结果。前端拿到的是三段Base64Url编码拼接成的完整字符串。签发代码一般这么写import jwt from datetime import datetime, timedelta, timezone SECRET_KEY your-super-secret-key-change-me ALGORITHM HS256 ACCESS_TOKEN_EXPIRE_MINUTES 30 REFRESH_TOKEN_EXPIRE_DAYS 7 def create_access_token(data: dict, expires_delta: timedelta | None None) - str: to_encode data.copy() expire datetime.now(timezone.utc) ( expires_delta or timedelta(minutesACCESS_TOKEN_EXPIRE_MINUTES) ) to_encode.update({exp: expire, type: access}) return jwt.encode(to_encode, SECRET_KEY, algorithmALGORITHM) def create_refresh_token(data: dict) - str: to_encode data.copy() expire datetime.now(timezone.utc) timedelta(daysREFRESH_TOKEN_EXPIRE_DAYS) to_encode.update({exp: expire, type: refresh}) return jwt.encode(to_encode, SECRET_KEY, algorithmALGORITHM)关于Payload的取舍我有一条铁律只放能唯一定位用户的信息绝不放密码、手机号、邮箱等敏感字段。JWT虽然带签名但它的Payload只是Base64编码任何人拿到令牌都能解码看到内容。你把手机号放进去等于把手机号明文写在了每一张“通行证”上一旦令牌泄露隐私也跟着泄露。subSubject字段放用户ID就足够了其他数据需要时拿ID再查。3.4 依赖注入一次鉴权全局生效FastAPI最舒服的地方就是这个Depends机制。写好一个get_current_user依赖之后任何接口只要在参数列表里声明一下FastAPI就会在请求进来时先执行依赖拿到当前用户再进入业务逻辑。from fastapi import Depends, HTTPException, status from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials import jwt from sqlalchemy.orm import Session from .. import models, security from ..database import get_db security_scheme HTTPBearer(auto_errorFalse) def get_current_user( credentials: HTTPAuthorizationCredentials Depends(security_scheme), db: Session Depends(get_db) ) - models.User: if credentials is None: raise HTTPException( status_codestatus.HTTP_401_UNAUTHORIZED, detail缺少认证信息 ) token credentials.credentials try: payload jwt.decode( token, security.SECRET_KEY, algorithms[security.ALGORITHM] ) except jwt.ExpiredSignatureError: raise HTTPException( status_codestatus.HTTP_401_UNAUTHORIZED, detail令牌已过期 ) except jwt.InvalidTokenError: raise HTTPException( status_codestatus.HTTP_401_UNAUTHORIZED, detail无效令牌 ) if payload.get(type) ! access: raise HTTPException( status_codestatus.HTTP_401_UNAUTHORIZED, detail令牌类型错误 ) user db.query(models.User).filter( models.User.id int(payload.get(sub)) ).first() if user is None: raise HTTPException( status_codestatus.HTTP_401_UNAUTHORIZED, detail用户不存在 ) return user依赖写完之后受保护的接口长这样from fastapi import Depends from ..dependencies import get_current_user router.get(/me) def get_me(current_user: models.User Depends(get_current_user)): return {id: current_user.id, username: current_user.username}跟着写一遍就能感受到业务接口里一行鉴权代码都不用写依赖注入把认证和业务彻底解耦了。想给某个接口加权限要么加一个继承get_current_user的新依赖要么在依赖里做角色判断改动非常集中。4. 令牌刷新续签这件事不能靠用户重新登录30分钟过期时间定得比较短是为了降低令牌泄露后的攻击窗口。但问题随之而来用户正在编辑一段长内容突然令牌过期了前端如果直接把用户踢回登录页体验就毁了。所以现代应用普遍用Access Token加Refresh Token双层结构。流程是这样Access Token有效期短用于每次请求的鉴权Refresh Token有效期长专门用来在Access Token过期之后申请新的Access Token。前端发现接口返回401就拿着Refresh Token去调刷新接口拿到新令牌后重放原请求。用户全程无感根本不需要再次输入密码。刷新接口的代码from fastapi import APIRouter, Depends, HTTPException import jwt from .. import security, schemas router.post(/refresh, response_modelschemas.TokenResponse) def refresh_token( request: schemas.RefreshRequest, ): try: payload jwt.decode( request.refresh_token, security.SECRET_KEY, algorithms[security.ALGORITHM] ) except jwt.ExpiredSignatureError: raise HTTPException(status_code401, detailRefresh令牌已过期请重新登录) except jwt.InvalidTokenError: raise HTTPException(status_code401, detail无效的Refresh令牌) if payload.get(type) ! refresh: raise HTTPException(status_code401, detail令牌类型错误) new_access_token security.create_access_token({sub: payload.get(sub)}) new_refresh_token security.create_refresh_token({sub: payload.get(sub)}) return schemas.TokenResponse( access_tokennew_access_token, refresh_tokennew_refresh_token, token_typebearer, expires_insecurity.ACCESS_TOKEN_EXPIRE_MINUTES * 60 )注意这里刷新之后我还会再发一个新的Refresh Token而不是沿用旧的那个。理由是轮换机制每次刷新都换新的Refresh Token就算旧的Refresh Token被偷了攻击者用它换到新令牌的同时合法用户下一次刷新时会发现旧Token已经不可用从而暴露令牌被盗的事实。不过要实现完整的Refresh Token轮换和检测需要把Token存到Redis或数据库做比对纯无状态是做不到的这是无状态架构必须付出的代价。5. 令牌撤销难题JWT最大的坑在这里网上对JWT抱怨最多的一点令牌在签发后无法主动作废。Session可以随手删掉Redis里的Key让用户立刻失效JWT做不到因为在过期之前它始终是合法的。那用户改密码、被封号、退出登录怎么办解法很多我用的是最简单直接的黑名单方案import redis from datetime import timedelta redis_client redis.Redis(hostlocalhost, port6379, decode_responsesTrue) def revoke_token(jti: str, expires_in: int): redis_client.setex(frevoked:{jti}, expires_in, 1) def is_token_revoked(jti: str) - bool: return redis_client.exists(frevoked:{jti}) 1在生成Token的时候给每个令牌加一个jtiJWT ID字段就是一个随机UUID。需要撤销的时候把这个jti写进Redis并设置一个与Token剩余寿命一致的过期时间然后校验逻辑里先查一下这个jti是否在黑名单里。这样既实现了主动撤销Redis里也不会积压垃圾数据——Token过期了对应的黑名单条目也跟着过期。这个方案有一个取舍要讲清楚它让JWT的“无状态”打了折扣因为每次鉴权都要查一次Redis。但你换来的是真正的撤销能力对于需要登出、改密、封号的业务来说这个代价值得付。如果你做的是纯内部系统、用户量不大甚至可以先把撤销逻辑放到内存里的一个集合中后面再换Redis。退出登录的接口就容易了from fastapi import APIRouter, Depends import uuid from ..dependencies import get_current_user from ..security import extract_jti, revoke_token router.post(/logout) def logout(current_user Depends(get_current_user), token_data: str Depends(extract_jti)): revoke_token(token_data, expires_insecurity.ACCESS_TOKEN_EXPIRE_MINUTES * 60) return {detail: 已退出登录}6. 安全细节绕过三个最典型的JWT漏洞JWT相关的漏洞在各类安全报告里反复出现主要有三类算法混淆、密钥太弱、Payload信任未经验证的数据。我自己在代码评审里见到最多的就是第一类。算法混淆攻击的套路是这样的服务端签发Token时用RS256非对称算法但校验的时候如果你允许客户端自己选算法攻击者把Header里的alg改成none或者HS256然后拿公钥当作HMAC密钥来签名服务端如果在校验时没锁死算法就会把这个假Token当真。防法很简单PyJWT的decode里强制指定algorithms列表jwt.decode(token, SECRET_KEY, algorithms[HS256])绝对不要写algorithmsNone或者接受Header里的任何值。第二个风险是密钥太弱。很多人图省事SECRET_KEY就写个“secret”或者“123456”攻击者可以跑字典攻击把密钥猜出来然后想签什么Token签什么。我这里使用一个长时间的随机字符串或者用命令生成python -c import secrets; print(secrets.token_hex(32))第三个风险是把不可信数据放进Payload并直接信任。比如Token里有个role字段前端反代篡改成admin后端若不查数据库直接按admin放行权限就炸了。更稳的方式是Token里永远只放sub用户ID角色、权限这些可变信息每次从数据库里取。还有一个容易被忽略的点是校验exp之外的声明。很多人只检查过期时间签发时间iatIssued At、令牌类型type、发行人iss这些都不做校验。我在前面代码里特意加了type字段校验就是为了防止有人拿Refresh Token去当Access Token用缩小了令牌被混用的可能性。类似的如果你的服务有多个端共用同一套密钥建议加上audAudience字段区分Web端、小程序端、App端让每个端只能用自己那一类的Token。7. 踩坑实录Uvicorn日志、时区、CORS这些幺蛾子最后分享几个我实际部署中遇到过的问题都是书上不写但现实里躲不开的。第一个Uvicorn日志丢失。部署时用uvicorn app.main:app运行代码里的print和loguru日志在终端里能看到但一旦用systemd或Docker跑起来日志就神秘消失了。后来搞清楚uvicorn的日志系统默认只输出它自己logging记录的内容你的应用如果直接用logging配置了root会被uvicorn的config覆盖。解决方式是给日志器起个独立名字import logging logger logging.getLogger(myapp)这样配置loguru或者logging的handler时只作用在“myapp”这个logger上不受uvicorn干扰。第二个时间戳时区问题。用datetime.utcnow()生成过期时间在某些旧环境下会拿到naive时间与timezone-aware的比较直接报错或者出现“明明没过期却提示过期”的诡异问题。我现在统一用datetime.now(timezone.utc)生成和校验都在UTC时区下进行前端展示的时候再转本地时区彻底避免混乱。第三个CORS配置。如果前端是SPA跨域请求被拦是最常见的问题。FastAPI里用CORSMiddlewarefrom fastapi.middleware.cors import CORSMiddleware app.add_middleware( CORSMiddleware, allow_origins[http://localhost:5173], allow_credentialsTrue, allow_methods[*], allow_headers[*], )注意allow_origins如果写[*]并且allow_credentialsTrue浏览器会直接拒绝带Cookie的请求这是浏览器的安全策略。要么明确列出源头要么不带凭证。第四个Windows下打包FastAPI项目。用PyInstaller打包时main.py里如果用了uvicorn.run启动经常遇到模块找不到的问题尤其是uvicorn的loop和http实现。我的做法是改成用python -m uvicorn启动并且在打包命令里带上--collect-all uvicorn这样事件循环相关的模块才会被正确打进去。以上这些坑都是我实际跑项目时一个个踩出来的很多问题排查到最后就是一个小细节但能卡你半天。照着今天这套方案搭一次能少走我走过的一半弯路。JWT真正用熟了你在任何语言、任何框架里再做认证思路都是一样的。最后再提醒一句密钥一定别写死在代码里环境变量、密钥管理服务、Docker Secret都行就是别提交到Git。

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号