【Web全栈进阶】JWT无状态认证:签发、校验、刷新
你学会了“签名手环”session登录状态存在服务器。今天升级成“数字令牌”——JWT身份自含、验签即信、服务器不用记任何状态。给裸奔了五篇的早报站API穿上衣服。本篇产出注册 / 登录 / 刷新三个接口 受保护接口的完整实现。含代码约130行。 太长不看版给想快速上手的你项目信息一句话说明本篇目标给FastAPI接口加上JWT认证代码行数~130行含注释依赖pyjwtpython-multipart核心功能注册 登录 刷新 受保护接口跑起来的命令uvicorn app.main:app --reload→ /docs里Authorize核心知识点JWT三段结构、无状态验证、access/refresh双令牌做完你能得到一套生产级的API认证方案核心认知JWT的“无状态”是礼物也是代价——服务器不记状态 无法主动吊销。解法就是双令牌短命access 长命refresh。一、先复习session手环与JWT令牌的区别session是“签名手环”登录后服务器在库里/内存里记一笔客户端每次带session_id来服务器查表验证。JWTJSON Web Token是“数字令牌”登录后服务器把身份信息用户id、过期时间签名进一段字符串客户端每次带着它来服务器验签即可——不用查任何表。对比session手环JWT令牌身份信息存哪服务器要查表令牌自己带着不用查表验证方式对表验签名水平扩展会话要共享存储任何服务器都能独立验证登出/踢人删服务器记录即可做不到令牌还在客户端 “无状态”是JWT的礼物也是它的代价没有服务器记录 无法主动吊销。业界标准解法就是本篇的组合拳短命access token15分钟 长命refresh token7天——泄露的access很快过期刷新能力掌握在你手里。二、两分钟概念课JWT拆开看一段JWT长这样三部分用点分隔部分内容作用header{alg:HS256}签名算法声明payloadsub用户id、typeaccess/refresh、iat签发时间、exp过期时间身份声明signature用服务器密钥对前两部分签名任何篡改都会让验签失败 核心认知payload是公开可读的只是base64编码不是加密——所以令牌里绝不能放密码等敏感信息只能放“身份标识时间”。安全靠的是签名不是隐藏内容。三、第0步依赖与配置pipinstallpyjwt python-multipart包作用pyjwt签发与验签不用自己实现HMACpython-multipart登录接口要兼容Swagger的Authorize表单FastAPI的OAuth2PasswordRequestForm需要它Settings加两个配置项app/config.pyclassSettings(BaseSettings):...jwt_secret:strdev-only-change-me# 生产环境务必用环境变量覆盖jwt_expire_minutes:int15# access token短命refresh_expire_minutes:int60*24*7# refresh token 7天User模型加密码列alembic装的git今天第一次兑现classUser(Base):...password_hash:Mapped[str]mapped_column(server_default)alembic revision--autogenerate-madd user password_hashalembic upgradehead这就是Alembic的全部意义加字段不用删库、不用重造数据。四、第1步签发——注册与登录app/routers/auth.pyapp/routers/auth.py —— 注册 / 登录 / 刷新 / 当前用户fromdatetimeimportdatetime,timedelta,timezoneimportjwtfromfastapiimportAPIRouter,Depends,HTTPExceptionfromfastapi.securityimportOAuth2PasswordRequestFormfrompydanticimportBaseModelfromsqlalchemyimportselectfromsqlalchemy.ormimportSessionfromwerkzeug.securityimportcheck_password_hash,generate_password_hashfromapp.configimportsettingsfromapp.depsimportget_sessionfromcore.modelsimportUser routerAPIRouter(prefix/auth,tags[auth])classRegisterBody(BaseModel):username:strpassword:strdefcreate_token(user:User,expires_minutes:int,token_type:str)-str:签发令牌payload只放身份标识和时间不放敏感信息nowdatetime.now(timezone.utc)payload{sub:str(user.id),type:token_type,iat:now,exp:nowtimedelta(minutesexpires_minutes),}returnjwt.encode(payload,settings.jwt_secret,algorithmHS256)router.post(/register)defregister(body:RegisterBody,session:SessionDepends(get_session)):注册密码哈希后入库第18篇的碎纸机existssession.scalar(select(User).where(User.usernamebody.username))ifexists:raiseHTTPException(status_code400,detail用户名已被占用)userUser(usernamebody.username)user.password_hashgenerate_password_hash(body.password)session.add(user)session.commit()return{id:user.id,username:user.username}router.post(/login)deflogin(form:OAuth2PasswordRequestFormDepends(),session:SessionDepends(get_session)):登录校验密码签发access refreshusersession.scalar(select(User).where(User.usernameform.username))ifnotuserornotcheck_password_hash(user.password_hash,form.password):raiseHTTPException(status_code401,detail用户名或密码错误)return{access_token:create_token(user,settings.jwt_expire_minutes,access),refresh_token:create_token(user,settings.refresh_expire_minutes,refresh),token_type:bearer,} 哈希加盐原样复用碎纸机还是那把OAuth2PasswordRequestForm让登录兼容Swagger的Authorize表单。五、第2步校验——依赖注入实现get_current_user第05篇的Depends模式现在派上大用场——写一个“谁在调用我”的依赖挂在需要登录的接口上即可。app/deps.py补充fromfastapiimportDepends,HTTPExceptionfromfastapi.securityimportOAuth2PasswordBearerfromsqlalchemy.ormimportSessionimportjwtfromapp.configimportsettingsfromcore.modelsimportUser oauth2_schemeOAuth2PasswordBearer(tokenUrlauth/login)defget_current_user(token:strDepends(oauth2_scheme),session:SessionDepends(get_session),)-User:从令牌解析出当前用户无效或过期一律401credentials_errorHTTPException(status_code401,detail登录状态无效或已过期)try:payloadjwt.decode(token,settings.jwt_secret,algorithms[HS256])exceptjwt.PyJWTError:raisecredentials_error usersession.get(User,int(payload[sub]))ifuserisNone:raisecredentials_errorreturnuseroauth2_scheme的额外红利 FastAPI发现接口依赖它后/docs右上角会自动出现Authorize按钮——在Swagger里登录一次所有受保护接口都能直接在线调试本篇验收的重头戏。⚠️注意oauth2_scheme只在deps.py里定义一次。auth.py如果也需要它从deps导入即可——不要重复定义否则tokenUrl会对不上。六、第3步受保护接口与刷新 早报站第一个受保护动作——登录后才能提交RSS源app/routers/sources.py改造frompydanticimportBaseModelfromapp.depsimportget_current_userfromcore.modelsimportSource,UserclassSourceBody(BaseModel):url:strrouter.post()defcreate_source(body:SourceBody,session:SessionDepends(get_session),user:UserDepends(get_current_user),# 门禁挂上):登录用户提交自己的订阅源sourceSource(urlbody.url,user_iduser.id)session.add(source)session.commit()return{id:source.id,url:source.url}/auth/me——我是谁接口前端登录态判断的标准工具app/auth.pyrouter.get(/me)defme(user:UserDepends(get_current_user)):return{id:user.id,username:user.username} 刷新接口——access过期后用refresh换新的app/auth.pyclassRefreshBody(BaseModel):refresh_token:strrouter.post(/refresh)defrefresh(body:RefreshBody,session:SessionDepends(get_session)):用refresh token换新access tokencredentials_errorHTTPException(status_code401,detail刷新令牌无效或已过期)try:payloadjwt.decode(body.refresh_token,settings.jwt_secret,algorithms[HS256])exceptjwt.PyJWTError:raisecredentials_errorifpayload.get(type)!refresh:raisecredentials_error usersession.get(User,int(payload[sub]))ifuserisNone:raisecredentials_errorreturn{access_token:create_token(user,settings.jwt_expire_minutes,access),token_type:bearer,}token的typeclaim是“用途隔离”refresh令牌只能换新access不能直接当access用——令牌之间的权限边界要靠自己画。修正说明原文refresh_token: str是查询参数会暴露在URL里已改为用Pydantic模型接收JSON请求体更安全也更符合规范。七、验收清单1.curlPOST /auth/register → 注册成功psql看password_hash是天书2.curlPOST /auth/login → 拿到access_token refresh_token3.curl/auth/me不带令牌→401Not authenticated4.curl/auth/me-HAuthorization: Bearer token→ 返回用户信息5. /docs里点Authorize登录 → POST /sources可直接调试受保护生效6. 伪造一个token随便改一位→4017. 全程无报错后提交Gitgitadd.gitcommit-mJWT认证注册登录 受保护接口 刷新八、常见报错这6个JWT的标配重点①ImportError: python-multipart is not installed 原因OAuth2PasswordRequestForm解析表单需要python-multipart。✅ 解法pipinstallpython-multipart FastAPI的报错提示会直接告诉你装它。②ModuleNotFoundError: No module named jwt 原因包名陷阱——装的是pyjwt导入的是jwt第13篇bs4的亲戚。✅ 解法pipinstallpyjwt③jwt.exceptions.InvalidSignatureError/ 所有令牌突然失效 原因换了JWT_SECRET——签名密钥变了旧令牌全废。✅ 解法生产环境的JWT_SECRET一旦定了就永远不要改改了 全员重新登录。 万一泄露改完必须接受“全员下线”。④ 接口明明写了user依赖不登录也能访问 原因函数签名里写了user参数但忘了 Depends(get_current_user)。✅ 解法检查Depends受保护接口必须显式声明依赖否则就是裸奔。⑤ExpiredSignatureError刚登录怎么就过期 原因access token 15分钟到期设计如此。✅ 解法走/auth/refresh换新前端在401时自动刷新重试后续联调实现。⑥TypeError: NoneType object is not subscriptable或查不到用户 原因payload的sub是字符串“1”你却session.get(User, 1)——类型不匹配查不到。✅ 解法int(payload[sub])转回整数第5节写法。签发时str()解析时int()两头要对齐。九、课后练习#练习难度提示1登出思考题无状态令牌怎么“踢人”研究两个方案——令牌黑名单服务器记下作废的jti或用户表存token_version签发时带上校验时比对⭐⭐⭐各自代价是什么2refresh轮换给User加token_version列refresh一次1旧refresh令牌立即失效⭐⭐⭐payload里带version claim3角色区分token里加role claimuser/admin管理员专属接口校验角色⭐⭐get_current_admin依赖4选做完整流程截图用/docs的Authorize走通“登录 → 提交订阅源 → 查看列表”三步⭐⭐发评论区 配套代码完整认证模块已上传GitHubpython_daily/【gitee仓库地址】

相关新闻

C++跨平台开发实战:从编译器差异到部署排坑指南

C++跨平台开发实战:从编译器差异到部署排坑指南

接手过一个挺折腾的项目:Windows上编译运行一切正常,一到Linux服务器上就崩溃,而且崩得很没规律。紧接着macOS上同事又报告中文乱码。那几天我基本在三个系统之间来回切,最后发现问题不在业务逻辑,而在最底层那批"…

2026/10/10 18:15:47 阅读更多 →
滑动窗口最大值:单调队列优化从O(nk)到O(n)的经典算法

滑动窗口最大值:单调队列优化从O(nk)到O(n)的经典算法

1. 这一题在LeetCode题库里的位置和价值题目名字一眼就能看出坑点:LeetCoce滑动窗口最大值。如果你在搜索引擎里看到这个拼写,别急着笑,其实它是LeetCode 239题“Sliding Window Maximum”的常见搜索变体。我在刷题群里见过不下三个人用这个拼…

2026/10/10 18:15:47 阅读更多 →
SpringBoot校园信息共享系统开发实战:从设计到部署的完整复盘

SpringBoot校园信息共享系统开发实战:从设计到部署的完整复盘

说到校园信息共享,很多人第一反应是想做二手交易、失物招领、活动通知这类功能集合。实际上你去看市面上的毕业设计和课程项目,这类题目出现的频率非常高,但大部分实现都停留在“能跑通”的水平——点开一个页面能发布信息,能登录…

2026/10/10 18:15:47 阅读更多 →

最新新闻

UMMA block scaling 原理与实战:矩阵乘加分块缩放量化部署指南

UMMA block scaling 原理与实战:矩阵乘加分块缩放量化部署指南

1. UMMA block scaling 到底在解决什么问题第一次看到“UMMA 的 block scaling”这个说法,很多人会以为是某个新框架的专属名词。其实把词拆开就清楚了:UMMA 通常指一类统一内存访问架构下的矩阵乘加运算单元,而 block scaling 指的是在矩阵运…

2026/10/10 22:13:01 阅读更多 →
e稿使用流程全指南 不同学科人群操作技巧分享

e稿使用流程全指南 不同学科人群操作技巧分享

e稿基础使用流程全梳理 e稿作为一站式AI论文写作软件,操作简单易上手,全流程无需复杂配置即可使用。本章节梳理当前版本的全流程操作步骤,后续版本迭代可能调整,具体以官方最新说明为准。

2026/10/10 22:13:01 阅读更多 →
退货换标真实案例:旺季退货怎么变回库存

退货换标真实案例:旺季退货怎么变回库存

旺季是卖家期待的季节,也是退货多的季节。很多卖家只盯着出库,忽略了逆向,结果货退回来堆在仓里,既占地方又压资金,旺季赚的利润被逆向悄悄吃掉。本文用一个典型卖家的经历,讲清楚退货换标怎么把损失变回库…

2026/10/10 22:13:01 阅读更多 →
选中俄中亚专线物流服务商,重点看这三个维度

选中俄中亚专线物流服务商,重点看这三个维度

选中俄中亚专线物流服务商,不用看太多花哨的介绍。核心看三个硬指标:口岸有没有自营仓库、清关团队是不是自有、保险配置够不够全。1. 口岸资源:有没有自营仓库,旺季见分晓中俄中亚陆运的核心节点在新疆口岸。霍尔果斯和乌恰是两个…

2026/10/10 22:13:01 阅读更多 →
私有仓库也敢画:GitDiagram 本地部署与 Token 配置全流程

私有仓库也敢画:GitDiagram 本地部署与 Token 配置全流程

私有仓库也敢画:GitDiagram 本地部署与 Token 配置全流程 【免费下载链接】gitdiagram Visualize any GitHub codebase: free interactive architecture diagrams and one-minute explainer videos. Replace hub with diagram in any GitHub URL. 项目地址: https…

2026/10/10 22:13:01 阅读更多 →
勒索病毒应急响应全指南:从隔离取证到数据恢复实战

勒索病毒应急响应全指南:从隔离取证到数据恢复实战

1. 中招瞬间的第一反应:先别急着拔网线先说个真实场景。某天凌晨两点,值班同事打来电话,声音都在抖:文件打不开了,后缀变成一串乱码,桌面上多了个 txt,大意是“你的文件已被加密,72小…

2026/10/10 22:12:00 阅读更多 →

日新闻

卫星轨道分类全解析:从LEO到GEO的选型逻辑与工程实践

卫星轨道分类全解析:从LEO到GEO的选型逻辑与工程实践

1. 从“卫星轨道分类”这个标题说起:为什么值得花时间搞懂第一次接触“卫星轨道分类”这个概念,很多人会觉得它离自己很远——不就是天上的星星怎么转吗?但如果你正在做航天任务规划、遥感数据接收、星座设计,甚至只是准备一场航天…

2026/10/10 0:00:39 阅读更多 →
Spring AOP 核心原理与实战:从概念到日志切面落地

Spring AOP 核心原理与实战:从概念到日志切面落地

1. 从一个真实痛点说起:为什么你的代码里到处都是重复逻辑刚入行那会儿,我写过一个用户管理模块,注册、登录、改密码、注销四个接口。每个接口里都塞了几乎一样的日志打印、参数校验、事务开启和提交。当时觉得没什么,能跑就行。直…

2026/10/10 0:00:40 阅读更多 →
Python招聘数据采集与分析可视化:从采集清洗到薪资技能城市可视化全链路

Python招聘数据采集与分析可视化:从采集清洗到薪资技能城市可视化全链路

简介:这是一套面向计算机相关专业学生与项目实战学习者的Python数据采集与分析可视化完整项目,以Boss直聘岗位数据为对象,适合用作毕业设计、课程设计或期末大作业。资源包共38个文件,约246KB,以13个py源码文件为核心&…

2026/10/10 0:00:40 阅读更多 →

周新闻

KT148A语音芯片外挂8002D功放的工程实践指南

KT148A语音芯片外挂8002D功放的工程实践指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/10 11:14:25 阅读更多 →
LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/10 1:36:08 阅读更多 →
ARM架构深度解析:从RISC设计理念到交叉编译实战

ARM架构深度解析:从RISC设计理念到交叉编译实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/10 11:14:58 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/10 5:23:50 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/9 21:32:20 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/10 10:38:42 阅读更多 →