尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

JWT鉴权原理与PyJWT实战:从签名算法到Token续签与安全防护

发布时间:2026/9/10 1:44:39

资讯中心
01
ARTICLE

JWT鉴权原理与PyJWT实战:从签名算法到Token续签与安全防护

JWT鉴权原理与PyJWT实战:从签名算法到Token续签与安全防护
1. JWT到底是什么先搞懂这套鉴权逻辑做过几年后端的人对JWT应该都不陌生。但说实话我发现很多刚接触的朋友对JWT的理解停留在“会用”层面甚至有不少人写了好几个月的接口被问到“JWT的Signature到底在防什么”时仍然答不上来。PyJWT是Python生态里最主流的JWT实现库之一。它底层依赖cryptography或pycryptodome来做签名和校验对外暴露的API却非常简洁——核心就两个方法encode和decode。正因为简洁很多人容易低估它的设计精妙程度也容易踩坑。在正式开始之前先把JWT的定位说清楚JWTJSON Web Token是一种开放标准RFC 7519定义了一种紧凑的、自包含的JSON数据传递格式。所谓自包含意思是token本身就能携带业务所需的信息比如用户ID、角色、过期时间服务端不需要查数据库去确认“这个token是谁的”。这跟传统Session方案有本质区别——Session是把会话数据存在服务器内存或Redis里客户端只拿一个随机的session_id而JWT则是把状态“存储”在token自身里。那JWT能解决什么问题最常见的场景就是无状态鉴权。用户登录成功后服务端签发一个JWT返回给前端前端在后续请求的Authorization头里带上它服务端验签通过直接从token里取出用户信息完成身份认证。整个过程不需要在服务端保存任何会话状态。适合谁来参考我的判断是这三类人第一刚接触后端鉴权、想理解JWT底层原理的初学者第二已经在项目里用JWT但被各种诡异问题折磨的开发者比如token明明没错却说签名无效第三需要对现有鉴权方案做安全加固的架构师。这篇文章不会只停留在“怎么用”我会把编码解码细节、签名算法选型、续签方案、常见攻击手法全部过一遍并且结合我实际踩过的坑来讲。2. 从结构拆解到核心原理JWT的三个组成部分想深入理解PyJWT首先要彻底搞懂JWT本身的结构。一个JWT长这样eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c肉眼看起来是一串乱七八糟的字符但用.分割后它其实正好是三个独立部分Header头部、Payload载荷、Signature签名。2.1 Header头部声明算法和类型第一部分是Header通常是一个很小的JSON对象包含两个字段alg签名算法和typtoken类型。{ alg: HS256, typ: JWT }alg决定了这个token的签名方式。HS256表示HMAC-SHA256属于对称签名RS256表示RSA-SHA256属于非对称签名。typ一般固定是JWT代表这是一个JWT格式的token。Header会先经过Base64Url编码作为最终token的第一段。注意是Base64Url而不是普通的Base64——两者的区别在于Base64Url会把替换成-把/替换成_并且去掉末尾的填充字符。原因很简单JWT经常出现在URL参数、HTTP Header里标准Base64中的和/在URL里有特殊含义容易造成解析错误。2.2 Payload载荷携带业务声明第二部分是Payload它承载JWT的“业务内容”也就是各种声明Claim。RFC 7519定义了几个保留声明Registered Claims但都不强制使用声明名含义说明issIssuer签发者标识token是谁签发的subSubject主题标识token所面向的主体一般是用户IDaudAudience受众标识token的接收方expExpiration Time过期时间数字时间戳过期后token无效nbfNot Before生效时间在此时间之前token不生效iatIssued At签发时间token的签发时间戳jtiJWT ID唯一标识防止重放攻击用的唯一ID除了这些保留声明你可以放任何自定义字段比如role、user_name、permissions之类的。但要记住一个铁律不论放什么字段Payload只是Base64Url编码而不是加密任何拿到token的人都能直接解码看到内容。所以绝对不要在Payload里放密码、手机号、身份证号等敏感信息。2.3 Signature签名防篡改的关键第三部分Signature是JWT安全性的基石。它是由Header、Payload、密钥secret和Header中指定的算法一起计算出来的。以HS256为例HMACSHA256( base64UrlEncode(header) . base64UrlEncode(payload), secret )也就是说把前两段用.拼接起来再用HMAC-SHA256算法和密钥做哈希运算得到的结果再做Base64Url编码就是第三段Signature。这个签名的作用有两层防篡改只要Header或Payload任何一个字节被修改验签时算出来的签名就和原签名不一致token直接判废。防伪造因为签名需要密钥攻击者拿不到密钥就无法伪造一个合法签名的token。可以把它类比成“信封上的火漆印章”——信封本身是透明的Payload可读但封印Signature是只有持有印章密钥的人才能盖出来的你拆开信封改一个字封印就对不上了。3. 实操第一步安装PyJWT与基础编码解码理论部分差不多了现在动手。我以当前最新的PyJWT 2.x版本为例如果你还在用1.x建议升级2.x修了不少安全问题并且API更清晰。3.1 安装环境与依赖选择安装非常简单pip install PyJWT但这里有个值得注意的细节PyJWT的加密算法支持分为两档取决于你装了哪些底层依赖。PyJWT会根据环境自动选择可用的算法。如果你只装了PyJWT本身它会顺带装上cryptography那么HS256、RS256、ES256等算法都可用如果你为了精简环境而没有装cryptography那就只能使用HMAC系列算法比如HS256。我个人的建议是在正常项目中直接安装cryptography因为RS256/ES256非对称签名在微服务架构里非常常见后面我会详细讲为什么。安装命令pip install PyJWT[crypto]这条命令会同时安装PyJWT和cryptography。3.2 最小可用示例签发与验证先来一个最直白的例子import jwt import datetime # 定义密钥生产环境绝对不要硬编码 SECRET_KEY your-secret-key # 构造payload payload { user_id: 10086, username: zhangsan, role: admin, exp: datetime.datetime.utcnow() datetime.timedelta(hours2), iat: datetime.datetime.utcnow() } # 签发token token jwt.encode(payload, SECRET_KEY, algorithmHS256) print(token) # 验证token try: decoded jwt.decode(token, SECRET_KEY, algorithms[HS256]) print(decoded) except jwt.ExpiredSignatureError: print(token已过期) except jwt.InvalidTokenError as e: print(ftoken无效: {e})这段代码做了三件事构造带过期时间的Payload、用HS256签名生成token、用同样的密钥验签并取出Payload。注意几个关键点exp必须是一个Unix时间戳整数或浮点数。PyJWT在2.x版本里不接受datetime对象直接作为claim值传入但官方建议用datetime.datetime.utcnow() timedelta(...)这种写法jwt.encode会自动将其转换成时间戳。实际上在2.x中传datetime对象是可以的encode内部会处理。但如果你的Payload里直接放的是整数时间戳那也没有任何问题。decode时必须显式指定algorithms[HS256]这是PyJWT 2.x的强制要求。如果不指定会抛DeprecationWarning并且在未来版本中会直接报错。这个设计是为了防止算法混淆攻击后面安全部分细说。decode默认会校验exp如果token过期了会抛出ExpiredSignatureError。3.3 手动拆解一个tokenBase64Url解码为了验证我对结构的理解我建议你把生成的token手动解码一下。我经常用这种方式来调试问题import base64 token eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxMDA4NiwidXNlcm5hbWUiOiJ6aGFuZ3NhbiIsInJvbGUiOiJhZG1pbiIsImV4cCI6MTcyMDAwMDAwMCwiaWF0IjoxNzE5OTk2NDAwfQ.abcdefg... header, payload, signature token.split(.) def base64url_decode(data): padding * (4 - len(data) % 4) return base64.urlsafe_b64decode(data padding) print(base64url_decode(header).decode()) print(base64url_decode(payload).decode())你会发现Payload里存的user_id、username、role直接明文可见。这就是我反复强调“别放敏感信息”的原因——信息泄露往往不是从你的服务器泄露的而是从token本身泄露的。4. HS256 vs RS256签名算法的选型逻辑PyJWT支持的签名算法很多但实际项目中90%的场景就是两种HS256和RS256。理解它们的区别和适用场景是做好鉴权设计的关键。4.1 HS256对称签名一台服务器背后的简单可靠HS256使用同一个密钥secret进行签名和验签。逻辑很简单我签发的token只有持有同样secret的人才能验证。优点是实现简单、性能好非常适合单体应用或单一信任域内的服务。缺点是签名密钥和验证密钥是同一个意味着任何需要验签的服务都必须持有这个secret。如果你的系统有多个服务都需要验签比如网关验签、用户服务签发、订单服务校验那这个secret就被分发到了所有地方任何一处泄露整个签名的安全性就崩了。4.2 RS256非对称签名多服务场景的正解RS256使用一对密钥私钥private key用来签发token公钥public key用来验证token。私钥只保留在签发服务比如认证中心手里其他服务只需要公钥即可验签。这带来的最大好处是公钥可以公开分发即使某个业务服务的公钥泄露了攻击者也拿它伪造不了token。因为伪造需要私钥而私钥只在签发端手里。在微服务架构中通常由一个统一的认证服务负责签发token其他业务服务只持有公钥做验签。这样即便某个业务服务被攻破攻击者也无法据此签发合法token给其他服务。我实际项目中的建议如果你的服务将来会做微服务拆分或者已经有多个后端服务需要共享鉴权直接上RS256。别图省事用HS256后期拆服务的时候再换算法牵涉所有存量token非常痛苦。4.3 ES256与EdDSA等其他算法ES256是基于ECDSA椭圆曲线数字签名算法的非对称算法它的优势是签名长度更短256位密钥生成约64字节的签名适合对token长度有极致要求的场景。它的密码学安全性和RSA同级别但在密钥生成和使用上有一点点门槛。EdDSAEd25519是更新的选择签名短、性能高、安全性设计更现代PyJWT 2.x已经开始支持。不过它的生态兼容性相对不如RSA成熟很多第三方服务比如某些API网关可能还不支持选它之前最好确认下游全部支持。我的选型建议表算法类型签名长度性能适用场景HS256对称32字节快单体应用、内部工具、信任域单一RS256非对称256字节较慢RSA运算微服务、多服务共享鉴权、第三方开放平台ES256非对称64字节快对token体积敏感的高并发场景EdDSA非对称64字节极快全栈自己掌控、无兼容性担忧5. 实战进阶完整实现Token续签与双Token方案热搜词里有个“jwt实现token续签”这是JWT落地时必然会遇到的问题。因为JWT一旦签发就无法撤销无状态特性而exp过期时间设短了用户体验差设长了又怕token泄露后长期有效于是各路团队的解法五花八门。我先说结论最标准、最常用的方案是“短期Access Token 长期Refresh Token”的双Token模型。5.1 为什么不能只用长期Access Token如果在Access Token里把exp设为7天那么这7天内只要token泄露比如被浏览器插件窃取、被日志打印出去攻击者可以持续盗用而服务端完全没办法主动撤销——因为JWT是无状态的服务端不记录这个token是否“已被注销”。你可能想通过黑名单方案来弥补就是把注销的token放在Redis里过期前都校验这确实可行但等于回到了Session的老路JWT无状态的优势就没了。5.2 双Token模型设计与实现我的实现思路是这样的Access Token短期有效比如30分钟。专用于业务接口的鉴权。因为生命周期短即使泄露攻击窗口也很小。Refresh Token长期有效比如7天或30天。专用于“续期”——当Access Token过期时客户端拿着Refresh Token去请求一个特定的刷新接口换一个新的Access Token回来。Refresh Token的签发和校验同样可以用PyJWT完成区别在于它通常会被存储在一个服务端可查询的地方比如Redis以便实现“主动吊销”——用户登出或者检测到异常时服务端直接把Refresh Token删掉这样就实现了“让JWT可以撤销”的效果。流程如下1. 用户登录 - 服务端生成 access_token (30分钟) 和 refresh_token (7天) 2. 服务端把 refresh_token 存一份到 Rediskey是用户idvalue是refresh_token 3. 客户端在请求头带 access_token 访问业务接口 4. access_token 过期 - 接口返回 401 5. 客户端拿 refresh_token 请求 /auth/refresh 6. 服务端验证 refresh_token 的签名和有效期并检查 Redis 里是否存在它 7. 校验通过 - 返回新的 access_token和新的 refresh_token可选 8. 校验失败 - 返回 401强制用户重新登录代码如下基于FastAPI风格但逻辑可平移到任意框架import jwt import datetime import redis SECRET_KEY your-secret-key REFRESH_SECRET_KEY your-refresh-secret-key r redis.Redis(hostlocalhost, port6379, decode_responsesTrue) def create_access_token(user_id: int, username: str, role: str) - str: payload { user_id: user_id, username: username, role: role, type: access, exp: datetime.datetime.utcnow() datetime.timedelta(minutes30), iat: datetime.datetime.utcnow() } return jwt.encode(payload, SECRET_KEY, algorithmHS256) def create_refresh_token(user_id: int) - str: payload { user_id: user_id, type: refresh, exp: datetime.datetime.utcnow() datetime.timedelta(days7), iat: datetime.datetime.utcnow() } token jwt.encode(payload, REFRESH_SECRET_KEY, algorithmHS256) # 存入Redis实现可撤销 r.setex(frefresh_token:{user_id}, 7 * 24 * 3600, token) return token def refresh_access_token(refresh_token: str): try: payload jwt.decode(refresh_token, REFRESH_SECRET_KEY, algorithms[HS256]) user_id payload[user_id] # 校验Redis里存在并且一致 stored_token r.get(frefresh_token:{user_id}) if stored_token ! refresh_token: raise jwt.InvalidTokenError(Refresh Token已被吊销) # 签发新的access_token new_access_token create_access_token( user_iduser_id, usernamepayload.get(username, ), rolepayload.get(role, user) ) return new_access_token except jwt.ExpiredSignatureError: raise Exception(Refresh Token已过期请重新登录) except jwt.InvalidTokenError: raise Exception(Refresh Token无效请重新登录)注意几个细节Refresh Token和Access Token用不同的密钥REFRESH_SECRET_KEY和SECRET_KEY这样两者互不干扰。即使Access Token的密钥泄露攻击者也无法拿它签发Refresh Token。在Payload里用type字段区分token类型解码时先检查type防止攻击者拿着Refresh Token去当Access Token用反之亦然这叫token类型混淆攻击的防护。refresh_access_token里的username和role是从旧Refresh Token里解出来的。如果你的业务对数据实时性要求高比如用户角色被改了这里就不应该从旧token里取而应该查数据库。5.3 Refresh Token轮换与重用检测再深入一层上面的方案如果Refresh Token泄露了攻击者也可能拿它去刷新。为了应对这种情况安全要求更高的系统会做Refresh Token轮换Rotation和重用检测Reuse Detection。轮换的意思是每次刷新的同时签发一个新的Refresh Token旧的就作废从Redis删掉存新的。这样做的好处是Refresh Token本身也变成“一次性”的即使泄露攻击者用一次之后它就失效了。重用检测的触发条件是如果服务端发现同一个Refresh Token被用了两次第一次刷新之后Redis里已存入新token第二次又拿旧的来刷新说明这个refresh_token很可能已经泄露防御策略是吊销该用户所有的Refresh Token强制重新登录。这两个机制合起来能在很大程度上缓解JWT“不可撤销”的固有缺陷。我在研发后台管理系统时就是这套逻辑实测能让账号被盗后的损失范围大幅缩小。5.4 续签的另一种思路滑动过期Sliding Expiration除了双Token还有一种叫“滑动过期”的简单方案每次请求时如果发现token的剩余有效期低于某个阈值比如还剩不到10分钟就签一个新的token返回前端感知到新token后替换旧的。滑动过期的实现比双Token简单但缺点也很明显它没法做到“注销立即生效”而且刷新动作和业务请求耦合在一起逻辑比较绕。我实际项目里只在用户无感登录的轻量场景里用过正式系统我还是推荐双Token。6. 安全攻防JWT伪造、破解与防御手段热搜词里有“jwt伪造”“jwt破解”这其实是安全方向的必考话题。我不赞成拿这套技术去攻击别人的系统但理解攻击原理是做好防御的前提所以这一章我们从“攻击者视角”拆解JWT最常见的几类问题然后给防御方案。6.1 算法混淆攻击把RS256降级成HS256这是JWT最经典的攻击方式之一PyJWT 1.x时代也曾经存在这个安全缺陷所以2.x才强制要求显式声明algorithms。攻击原理是这样的如果你的服务端使用RS256非对称私钥在认证服务业务服务用公钥验证签名。攻击者把一个原本用RS256签名的token把Header里的alg字段改成HS256然后尝试用公钥作为HMAC密钥来重新签名。如果服务端没有限制算法而是“看Header里写了什么算法就用什么算法验证”那么攻击者就能用公开可获取的公钥作为“密钥”伪造出签名合法的token。这是完全脱离安全设计的逻辑漏洞。防御方式就一条验签时显式指定algorithms列表比如jwt.decode(token, public_key, algorithms[RS256])这样即使攻击者把Header改成HS256PyJWT也会发现algorithm不在允许列表里直接拒绝。6.2 弱密钥爆破别再拿“secret”当密钥HS256的安全性完全取决于密钥强度。如果密钥是secret、password、123456这类弱口令攻击者完全可以下载一个常用密钥字典把字典里的每个词都当作key去尝试验签。如果某个词验签通过说明密钥就是它然后攻击者就能任意伪造token。我见过一些内网系统密钥就是secret这等于把大门钥匙贴在门框上。防御方式密钥长度至少32字节256位并且用随机生成器生成不要用人类可读的单词。推荐用secrets.token_urlsafe(32)生成import secrets secret secrets.token_urlsafe(32) print(secret) # 例如: pB9aHl3nCQkR6zUvYwX1sTgJmEoNqLc0vR7xF4AbD8I不要把密钥硬编码在代码里。用环境变量、配置中心或密钥管理服务如Vault、KMS存储。实际上很多JWT破解工具的原理就是这个——离线下载大量token样本然后跑字典爆破。密钥强度不够防什么都是白搭。6.3 敏感信息泄露Payload是透明的在2.2节我已经强调过Payload不是加密的。但我在实际审查代码时还是经常看到有人往Payload里塞邮箱、手机号甚至密码哈希。这个问题的危险在于JWT经常出现在URL、浏览器LocalStorage、请求日志、中间件日志里。任何一个环节的日志泄露都等于把用户敏感信息明文暴露了。两条铁律只放“非敏感但业务需要”的字段user_id、username、role。如果需要在token里传递加密信息请使用JWEJSON Web Encryption这是另外一个标准PyJWT目前不直接支持JWE。或者更务实的做法把敏感数据放服务端缓存token里只放一个引用ID。6.4 其他常见攻击手法与防御清单攻击类型攻击路径防御措施重放攻击截获旧token在有效期内重复使用缩短exp启用Refresh Token轮换和使用检测对高安全接口做请求时间戳校验中间人窃听通过HTTP明文传输截获token全站启用HTTPSSecureHttpOnly标记CookieXSS窃取通过前端脚本从LocalStorage读取token优先使用HttpOnly Cookie而不是LocalStorage存储token对前端输入做XSS过滤日志泄露token被打印到服务端日志禁止对token字段打日志用filter在日志框架层脱敏跨站请求伪造(CSRF)利用Cookie自动携带机制发起请求Cookie方案需配合CSRF TokenHeader方案天然免疫CSRF6.5 实操安全检查脚本最后分享一个我用来审查自己系统的检查脚本思路你可以照着核对自己的项目# 1. 确认算法白名单是否强制 # 搜索代码中的 jwt.decode( 调用检查是否都传了 algorithms[...] grep -rn jwt.decode( your_project/ # 2. 确认密钥是否泄露在代码库 grep -rn SECRET_KEY\s*\s*[\][a-zA-Z0-9] your_project/ grep -rn secret\s*\s*[\]secret your_project/ # 3. 确认token没有出现在日志里 grep -rn token your_project/ --include*.py | grep log # 4. 检查payload里是否有敏感字段email, phone, password等 grep -rn password\|phone\|email your_project/ --include*.py | grep payload这套检查我基本上每次Code Review前都会跑一遍能快速暴露大部分低级但致命的问题。7. 常见报错与排查技巧实录最后这部分是我实际用PyJWT时遇到频率最高的几类问题每个都对应具体的报错信息和排查思路。建议收藏将来遇到问题可以直接翻。7.1DecodeError: Not enough segments报错场景传给jwt.decode的token不是一个合法的三段结构。排查思路检查token是否被截断打印前确认完整获取。检查是否把Bearer前缀一起传进来了——很多新手会把Authorization: Bearer eyxxx...整段传给decode正确做法是先去掉Bearer前缀auth_header request.headers.get(Authorization, ) token auth_header.replace(Bearer , ).strip()这个问题我见得太多了一半以上的Not enough segments都是没有去掉Bearer前缀导致的。7.2ExpiredSignatureError: Signature has expired报错场景token的exp时间已经过了。排查思路检查服务器时间和签发token的时间是否一致尤其是容器化部署场景容器时区问题会造成误判。确认exp用的是UTC时间戳而不是本地时间。用datetime.datetime.utcnow()生成的时间戳是UTC但如果你的服务器设在东八区却用datetime.datetime.now()生成然后直接加减时长那么token的实际过期时间会差8小时。我的习惯是统一约定所有时间戳都用UTC生成展示时再转本地时区。7.3InvalidSignatureError: Signature verification failed报错场景签名验证失败token被人改过或者验签用的密钥不对。排查思路先用在线解析工具就是热搜词里的“jwt在线解析”把token的三段解码看看Payload是否被改动过。确认服务端环境和签发环境用的是同一个密钥。密钥有环境差异开发/测试/生产是高频事故。检查密钥末尾是否有意外的换行符或空格从环境变量或配置文件读取密钥时注意别把换行符读进key里。7.4DecodeError: Invalid padding报错场景Base64Url解码时padding不对。排查思路这个报错大多数是因为token在传输过程中被URL编码处理过导致-和_被还原成和/了。在接收端做一下字符串反处理或者确保传输过程中不对token做额外的URL编码。PyJWT内部在解码时其实会处理padding但如果你手动pre-decode后传入就需要注意细节。7.5 关于“在线解析”经常有人用在线工具解析JWT方便确实方便。但我必须提醒一句在线解析工具的网站服务器会“看到”你的token内容。如果你的token里含有任何敏感信息即便只有user_id都会在第三方服务器留存记录。生产环境的token还是用本地脚本解析吧Python几行就能搞定别图省事。7.6 密钥轮换注意事项我最后想讲的是密钥轮换。很多人设计了密钥之后就不再管它直到某天密钥泄露才临时抱佛脚。比较稳妥的做法是给密钥加一个kidKey ID标识headers { kid: 2024-key-v2 } token jwt.encode(payload, SECRET_KEY, algorithmHS256, headersheaders)验签时根据kid选择对应的密钥这样密钥轮换期间新旧token都能正常验签过渡期结束再把旧密钥移除即可。JWT标准本身就支持kid头PyJWT也完整支持用起来很简单。在真实项目里这一套组合拳打下来JWT的基本功就算扎实了。这套东西看起来简单但每一个细节背后都有实际踩坑的代价。如果你正打算在项目里引入JWT或者正在被续签问题、安全问题困扰建议按这篇文章的路子一步步过一遍遇到卡点再回来翻对应的章节。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。