JWT 签名密钥爆破与算法层攻击:HS256 弱密钥攻防实战

发布时间:2026/9/17 21:15:27
JWT 签名密钥爆破与算法层攻击:HS256 弱密钥攻防实战 在授权的渗透测试或者CTF赛场上拿到一个JWTJSON Web Token之后大多数人的第一反应是把它三段拆开、Base64解出payload然后改掉里面的user_id或者role字段再拼回去发出去——结果服务端返回401。这时候很多人才意识到payload里那点东西根本改不动真正卡住你的是第三段那个签名。而一旦确认签名是HS256这类对称算法接下来最自然的动作就是这个签名用的密钥有多强能不能爆出来。这篇文章就围绕JWT与爆破这条线把签名段为什么值得爆、爆破背后的HMAC机制、一次完整的复现流程、比爆破更省事的几条算法层路径以及作为开发方怎么把这件的成本抬到不划算逐层讲清楚内容偏向授权自查场景适合做后端鉴权、做安全自查、以及打CTF的同学参考。1. 拿到Token先别急着爆破三段结构里只有一段值得动1.1 前两段是Base64URL编码不是加密很多人第一次看到JWT那串眼熟的长字符串会下意识以为它是加密过的。实际上打开一看就明白eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9这一段Base64解出来就是{alg:HS256,typ:JWT}。第二段payload同理不管里面塞的是用户ID、角色、过期时间还是自定义声明全都是明文只是做了一次Base64URL编码而已。Base64URL和标准Base64的区别很小就是把换成-、/换成_并且把末尾的填充去掉。这个细节后面会反复提到因为填充处理不当是签名校验玄学失败的头号原因。搞清楚这一点之后结论就很直接header和payload没有任何机密性可言任何人拿到token都能读但没有人能改——因为改了之后签名就对不上。所以当你在授权测试中拿到一个token第一步永远不是爆破而是先解码看header里的alg是什么。如果alg是none那连爆破都省了如果是RS256爆破的目标是私钥字段上根本不成立只有HS256、HS384、HS512这类HMAC系算法才存在用一份字典去猜密钥这种可能。1.2 签名段的密钥才是唯一被保护的东西JWT的设计逻辑里整个token的完整性全靠签名来兜。签名段的计算方式简单说就是把前两段用.拼起来扔进一个带密钥的哈希函数输出的结果再做一次Base64URL。以HS256为例它的运算就是signature HMAC-SHA256(secret_key, base64url(header) . base64url(payload))这个式子里有三个输入header、payload、key。前两个对攻击者是完全公开的因为都在token里明明白白写着。那么我能不能伪造一个token这个问题就等价于我能不能拿到那个key。key一旦泄露攻击者就能给自己签发任意payload的token把user_id改成管理员、把过期时间改成十年后服务端验签一样通过。于是整个JWT的安全强度被压缩到了一个点上这个key有多难猜。这就是爆破这个词能成立的全部理由。它不是什么高深技巧本质就是拿一份候选密钥字典逐个去算HMAC看算出来的结果和token里带的签名是否一致。一致就命中不一致就下一个。因为没有次数限制、没有速率限制、不需要跟服务端交互整个过程可以完全离线完成——这也是它比猜密码危险得多的原因。1.3 一次手工拆解从原始Token看到可攻击面我习惯的做法是先用最小的工具把token摊开。一段典型的token长这样eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMDAxIiwicm9sZSI6InVzZXIiLCJleHAiOjE3MzU2ODk2MDB9.7Jh3kR9xLmQ2vY8pN4cT1sB6wF0aE5uZ拆成三段分别处理第一段解码得到{alg:HS256,typ:JWT}确认是对称算法可爆。第二段解码得到{sub:1001,role:user,exp:1735689600}能看到用户标识、角色和过期时间。第三段是32字节的HMAC-SHA256结果做Base64URL长度固定43个字符去掉填充后。这里有个经验性判断如果签名段的长度是43字符左右基本就是HS256如果是342字符以上那是RS256/ES256这类非对称算法。非对称算法的签名很长因为它是用私钥对整个数据做数学运算而不是算哈希。看到长签名就别浪费时间在字典上了方向应该转向算法混淆或者公钥相关的路径这部分在第4节展开。另外提一句header里如果出现kid字段说明服务端可能是用这个key id去密钥库里找密钥的这又是一个独立的攻击面和爆破是两条线。把token摊开看五分钟能省掉后面几个小时的瞎试。2. HS256背后的HMAC机制弱密钥为什么会被离线打穿2.1 签名到底是怎么算出来的HMAC这个名字叫带密钥的哈希消息认证码原理不复杂。以HMAC-SHA256为例它做的是两次哈希HMAC(K, M) SHA256( (K ⊕ opad) || SHA256( (K ⊕ ipad) || M ) )其中K是把密钥补齐到64字节SHA256的块大小后的结果ipad是0x36重复64次opad是0x5c重复64次||表示拼接。这个双层结构的作用是防止长度扩展攻击同时让密钥以某种方式融进哈希的中间状态使得没有密钥就无法复现输出。对爆破来说这个结构带来一个非常关键的性质每一次尝试都是一次独立的、可以并行计算的运算彼此之间没有任何依赖。这意味着你可以把字典切成一万份扔给一万个CPU核心或者几千个GPU线程同时算速度线性叠加。这正是爆破JMAC比爆破密码快得多的根本原因——猜密码要跟服务器交互每次有网络延迟和限流猜JWT密钥是纯本地数学运算一次一块GPU每秒能试上亿次。再补一点验签的时候服务端拿到token也会用同样的方法算一遍签名然后和token里的签名做比较。这里有个实现细节——比较必须用常量时间比较比如Python的hmac.compare_digest否则会有时序侧信道。不过对爆破来说这个细节无关紧要因为爆破根本不需要跟服务端通信。2.2 离线爆破能成立的两个前提爆破要成功同时满足两个条件缺一不可。第一个前提签名算法是对称的。只有HS系算法HS256/HS384/HS512用的是同一把密钥签名和验证。RS256用的是私钥签名、公钥验证攻击者拿不到私钥字典里放什么都没用因为需要验签的公钥人家已经公开了而签名需要的是私钥。第二个前提密钥本身的熵不足以抵抗字典攻击。熵这个词听着抽象换成大白话就是这个密钥是从多大的池子里选出来的。如果密钥是开发者随手敲的secret、mykey123、company2024那它就在人类常用词的高频区任何一份像样的字典都能命中。如果密钥是32字节的密码学随机数候选空间是2^256那就算把全世界的算力堆起来也算不完。现实中的问题恰恰出在第一个前提上。我看过的项目里HS256的密钥来源五花八门有的是配置文件里写死的jwt_secret有的是项目名加年份有的是把公司域名当密钥还有的直接用了默认示例代码里的your-256-bit-secret。这类密钥的共同特点是——它们都能在高频字典的前几万行里找到。2.3 不同密钥形态的爆破成本量级为了让你对要花多久有个直观感觉我按几种典型密钥形态做了个估算。前提是HMAC-SHA256的运算速度这里按消费级高端显卡约每秒2亿次尝试来算不同显卡差距很大这个数量级仅供参考字典命中假设是如果密钥在字典里就能命中。密钥形态候选空间纯穷举耗时GPU 2亿次/秒实际可行性高频弱口令secret、123456约10^4毫秒秒级命中8位纯小写字母26^8 ≈ 2.09×10^11约17分钟完全可行8位大小写数字62^8 ≈ 2.18×10^14约12.6天有算力就能做12位大小写数字62^12 ≈ 3.2×10^21约50万年实际不可行32字节随机256位2^256宇宙尺度数学上不可行看这张表要抓住一条线从8位随机到12位随机只多了4个字符耗时从十几分钟跳到了几十万年。这就是为什么防守方的结论永远是用足够长的随机密钥而不是用复杂一点的密码。至于怎么生成、怎么存第5节展开。还有一点值得强调如果密钥是从一份包含数万条常见弱口令的字典里选的那实际耗时不是穷举耗时而是字典扫描耗时。一份20万行的字典在2亿次/秒的速度下理论满扫时间不到1毫秒的千分之一——现实中真正的瓶颈是磁盘IO和字典加载不是算力。3. 在授权环境里复现一次完整的密钥爆破这一节的所有操作前提都是你自己搭建的靶场环境、或者客户书面授权的测试范围。未经授权对他人系统做这件事性质完全不同这个边界必须守住。3.1 准备token、字典、算力开始之前要凑齐三样东西。一是有明确来源的token。最好是能确认它用的就是HS256的从header里直接读。如果你拿到的token是已过期的也不影响爆破——签名计算跟exp字段无关exp只影响服务端验签后的业务判断。二是字典。字典的质量比大小重要得多。我的常用组合是三份叠加一份通用弱口令表覆盖secret、password、123456这类一份项目相关的自定义词表项目名、公司名、域名、年份的各种大小写和连接符组合一份规则变形表。第二份往往才是命中的关键因为很多团队真的会用项目名年份当密钥。想构造自定义词表最直接的办法是收集项目相关的关键词然后用大小写变形、年份后缀、常见连接符-、_、.做排列组合。三是算力。不要用Python做生产级爆破它的速度是每秒几万次量级和GPU差四个数量级。Python只适合写个演示脚本验证思路真正跑的时候要上hashcat。3.2 用Python写一个最小可用的爆破脚本先用二十行代码把逻辑跑通确认自己的理解没问题这一步很有必要——很多人在离线的脚本里调试思路比在hashcat里瞎试快得多。import base64 import hmac import hashlib def b64url_decode(s: str) - bytes: # 补齐 Base64URL 缺失的填充 s * (-len(s) % 4) return base64.urlsafe_b64decode(s) def b64url_encode(b: bytes) - bytes: return base64.urlsafe_b64encode(b).rstrip(b) def crack(token: str, wordlist_path: str) - str | None: header_b64, payload_b64, sig_b64 token.strip().split(.) signing_input f{header_b64}.{payload_b64}.encode() target_sig b64url_decode(sig_b64) with open(wordlist_path, r, encodingutf-8, errorsignore) as f: for line in f: candidate line.rstrip(\n) calc hmac.new(candidate.encode(), signing_input, hashlib.sha256).digest() if hmac.compare_digest(calc, target_sig): return candidate return None if __name__ __main__: tk eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMDAxIn0.xxxxx hit crack(tk, wordlist.txt) print(命中密钥:, hit if hit else 未命中)这段代码里有两个地方容易写错我第一次写的时候都踩过。第一个是b64url_decode里的填充补齐。JWT用的是Base64URL且去掉了Python的urlsafe_b64decode要求长度是4的倍数否则直接抛异常。 * (-len(s) % 4)这个写法是补到4的倍数的最短方式-len(s) % 4在Python里的负数取模行为刚好满足需求。第二个是compare_digest。虽然爆破场景下用也不会出错但养成用常量时间比较的习惯是好事尤其是你在读别人的代码或者写自己的验签服务时。3.3 换hashcat把速度拉起来思路验证通过之后就该上真正的工具了。hashcat对JWT有专门的支持模式号是16500。# 把 token 存到文件里一行一个 echo eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMDAxIn0.xxxxx jwt.txt # 基础字典攻击 hashcat -m 16500 -a 0 jwt.txt wordlist.txt # 带规则变形字典条目会按规则扩展 hashcat -m 16500 -a 0 jwt.txt wordlist.txt -r rules/best64.rule # 掩码攻击8 位小写字母穷举 hashcat -m 16500 -a 3 jwt.txt ?l?l?l?l?l?l?l?l # 查看结果 hashcat -m 16500 jwt.txt --show-a 0是纯字典模式-a 3是掩码模式也就是穷举。?l代表小写字母?u大写?d数字。规则文件的作用是把字典里的每个词做变形比如secret扩展成secret、Secret、secret1、secret2024等等能显著提升命中率。提示hashcat 跑起来之后先用--status观察一下速度如果显示的是几万 H/s说明在用 CPU 或者显卡没被正确识别正常 GPU 的 HMAC-SHA256 速度应该在百万到亿的量级。速度不对就先排查驱动别干等。除了hashcatjwt_tool也是个好选择它的优点是接口更友好直接把token喂给它就行python3 jwt_tool.py token -C -d wordlist.txt-C表示crack模式-d指定字典。它在后台其实就是反复做HMAC计算胜在不用自己转格式。3.4 命中之后的三步验证拿到候选密钥之后别急着下结论按三步走。第一步用密钥重新签一个token。把payload里的role改成adminexp改成远期时间用命中的密钥算签名拼成新token。这一步能验证密钥确实可用。第二步把新token发给服务端。如果返回200并且拿到了管理员权限的数据说明漏洞成立。这一步必须在授权范围内做。第三步确认密钥来源写进报告。把命中的密钥和字典里的位置记下来如果能判断出这个密钥的类型比如是项目名变形还是通用弱口令对修复建议的针对性很有帮助。这里有个容易忽略的点有时候命中的密钥不止一个。因为HMAC有一个性质如果两个不同的密钥恰好补零后的部分相同或者字典里有重复项都可能出现多命中。正常情况下会停在第一个命中但如果你的脚本没做提前退出可能会收集到多个。多命中时优先选人类可读的那个那大概率就是真实密钥。4. 爆破之外的几条低成本路径算法层的经典缺陷密钥爆破只是JWT攻击面里的一条。实际测试中很多时候根本不需要爆——算法层和头部参数层的缺陷成本更低、命中率更高。4.1 alg字段置为none这是JWT历史上最出名的实现缺陷。原理很简单早期一些JWT库在验签时会读取header里的alg字段来决定用什么算法验证。如果把alg改成none同时把签名段留空某些库会认为用户指定了不需要签名于是直接通过验证。构造出来的token长这样eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiIxMDAxIiwicm9sZSI6ImFkbWluIn0.注意最后那个点后面是空的。这个套路的有效性完全取决于服务端用的库和版本。现代的、维护正常的JWT库基本都默认拒绝none或者要求显式配置一个允许无签名的开关。但如果项目用的是很老的版本或者自己手写的验签逻辑这个路径就值得一试。4.2 RS256与HS256的算法混淆这条路径比none更精妙也更值得理解。场景是服务端本来用RS256非对称私钥在服务端公钥是公开的可能放在/.well-known/jwks.json或者某个接口里。攻击者拿不到私钥按理说没法伪造签名。但如果服务端的验签代码是根据header里的alg选算法攻击者就可以把header的alg改成HS256然后用那个公开的公钥字符串本身作为HMAC密钥去签名。服务端收到后如果它按header说的用HS256验证就会拿公钥这个字符串当HMAC密钥来算——而攻击者手里正好有这个公钥于是签名对上了。# 演示逻辑alg 改成 HS256用公钥字符串作为 HMAC 密钥 import hmac, hashlib, base64 def b64url(b): return base64.urlsafe_b64encode(b).rstrip(b) public_key_pem open(public.pem, rb).read() # 从公开渠道拿到的公钥 header b{alg:HS256,typ:JWT} payload b{sub:1001,role:admin} signing_input b64url(header) b. b64url(payload) sig hmac.new(public_key_pem, signing_input, hashlib.sha256).digest() forged (signing_input b. b64url(sig)).decode() print(forged)这条路的根因是验签时信任了不可信输入header来决定验证方式。修复方法也很明确算法必须在服务端写死不能读token里的alg。4.3 kid、jku、x5u这些头部参数的注入面header里除了alg还有几个字段经常被忽略。kidKey ID用来指示服务端去密钥库里找哪把钥匙。如果服务端的实现是拿kid去文件系统里读文件或者把kid拼进SQL查密钥那么kid就成了注入点。经典场景是kid设为../../../../dev/null让服务端读到空文件于是密钥变成空字符串攻击者用空密钥签名即可通过。另一个变体是kid拼进SQL走SQL注入把密钥查出来。jku和x5u指向一个JWKS或证书的URL。如果服务端不加白名单就去这个地址拉公钥攻击者就可以把jku指向自己控制的服务器放上自己的公钥然后用对应的私钥签名。修复方式是把可信域名做成白名单。4.4 各条路径的成本对照把这几条放在一起比较一下方便你按优先级排测试顺序。路径前提条件测试成本常见程度弱密钥爆破HS 系算法 弱密钥中需要字典和算力高algnone老版本库或手写验签极低中新库已修算法混淆 RS→HS服务端按 header 选算法低中kid 注入kid 参与文件/SQL 查询低低jku/x5u 劫持未做域名白名单低低测试顺序我的习惯是先看header里有没有kid/jku有就试注入再试algnone改一下成本几乎为零然后试算法混淆需要确认原本是RS最后才回到爆破。把成本低的放前面因为很多时候根本轮不到爆破。5. 把攻击成本抬上去签名与声明校验的落地清单站在开发这一侧目标不是让攻击不可能而是让攻击不划算。下面这几条是我在自己项目里实际落地过的按重要性排序。5.1 密钥强度与生成方式密钥必须是从密码学安全的随机源里取出来的足够长的随机值。具体怎么做# 生成 32 字节256 位的随机密钥Base64 输出 openssl rand -base64 32 # 或者用 Python python3 -c import secrets, base64; print(base64.urlsafe_b64encode(secrets.token_bytes(32)).decode())绝对不要做的事把项目名、公司名、域名、年份当密钥用示例代码里的默认值把密钥提交进代码仓库。推荐的做法是通过环境变量或者密钥管理服务注入代码里只读环境变量配置文件里不放明文。再补一条经验密钥要有轮换机制。轮换的核心难点是轮换期间新旧token都能验常见做法是在验证时按顺序尝试多把密钥新的优先签发时只用最新的。这个逻辑要提前设计不要等到出事才想。5.2 算法白名单必须写死这是防算法混淆的关键。无论用哪个语言的JWT库验签时都要显式指定允许的算法列表而不是从header里读。以Python的PyJWT为例import jwt # 正确算法写死 payload jwt.decode( token, keySECRET, algorithms[HS256], # 只允许 HS256 options{require: [exp, iat]}, ) # 错误信任 header 里的 alg payload jwt.decode(token, keySECRET, algorithmsNone)algorithms这个参数的作用就是白名单。如果token的header写的是RS256或者none而白名单里只有HS256库会直接抛异常。这一行代码能挡掉none和算法混淆两大类问题。5.3 声明字段一个都不能省payload里的声明字段不是可有可无的装饰每一个都对应一类攻击。声明作用不校验的后果exp过期时间token 永久有效泄露后无法止损iat签发时间无法判断 token 是否被提前签发nbf生效时间无法限制 token 的可用窗口aud受众给 A 服务签的 token 能拿去调 B 服务iss签发方无法区分 token 来源aud这个字段特别容易被忽略。在微服务架构里如果所有服务共用一把密钥但都不校验aud那么给低权限服务签发的token可以直接拿去调高权限服务。这个问题的修复成本很低——解码时加一个audience参数就行。另外要提醒的是不要把敏感信息放进payload因为它就是明文。有些团队会把用户的手机号、内部ID、权限列表全塞进去等于把这些数据公开。5.4 token续签的两种做法与各自的坑前端会话过期是个绕不开的问题续签的主流做法有两种各有各的坑。第一种是单token 滑动过期。每次请求都判断token快过期了就签一个新的返回给前端。问题是这会让token的有效期被无限延长一旦泄露攻击者只要持续发请求就能一直续命。我的做法是加一个绝对过期时间上限比如最多续到签发后7天。第二种是双tokenaccess refresh。access token短有效期15分钟到2小时refresh token长有效期但只能用来换新token且服务端要记录refresh token的状态以便撤销。这套方案安全性更高代价是需要一个存储来管理refresh token。def issue_tokens(user_id: str) - dict: now int(time.time()) access jwt.encode( { sub: user_id, aud: api, iat: now, exp: now 1800, # 30 分钟 typ: access, }, SECRET, algorithmHS256, ) refresh jwt.encode( { sub: user_id, iat: now, exp: now 7 * 86400, # 7 天 typ: refresh, jti: secrets.token_urlsafe(16), # 用于撤销 }, REFRESH_SECRET, # 和 access 用不同密钥 algorithmHS256, ) return {access_token: access, refresh_token: refresh}这里有个关键点access和refresh要用不同的密钥。因为access token会被频繁发到各个服务暴露面大refresh token只在认证服务这一处使用。用不同密钥能避免一个服务的密钥泄露导致refresh也被伪造。5.5 SPA里的存储位置与验证码接入单页应用的token存哪里是个老生常谈但一直有争议的问题。三种选择localStorage实现最简单但任何XSS都能读到一旦有XSS就等于token直接泄露。内存变量刷新页面就丢需要配合refresh token恢复会话安全性最好但实现麻烦。HttpOnly CookieJS读不到能防XSS窃取但需要处理CSRF并且跨域场景下配置麻烦。我的实际选择是access token放内存refresh token放HttpOnly Secure SameSite的Cookie。这样XSS拿不到长效凭证CSRF通过SameSite和额外的CSRF token防住。这套组合的实现成本不算高安全性提升明显。验证码的接入位置也值得说一句。很多人只在登录接口加图形验证码这对防JWT爆破没什么用——因为JWT爆破是离线的根本不碰你的登录接口。验证码真正该防的是高成本的在线攻击比如撞库、短信轰炸。它和JWT密钥强度的关系是正交的别把两者混为一谈。真正能防住密钥爆破的只有密钥足够随机这一件事。顺带提一下登录接口的限流策略也应该和JWT设计一起考虑。比如同一IP一分钟内失败超过10次就锁定一段时间这个策略能有效降低在线撞库的效率但对离线爆破依然无能为力。6. 实战里踩过的坑与排查记录前面讲的都是应该怎么做这一节讲讲实际做的时候会遇到什么。6.1 Base64URL填充缺失导致的签名校验玄学失败这个坑我踩过不止一次。现象是脚本里手工算出来的签名和服务端返回的结果偶尔一致、偶尔不一致同样的token有时候能过有时候不能过。第一次排查花了不少时间最后发现是填充处理的问题。JWT的Base64URL去掉了末尾的但不同的库在解码时对填充的容忍度不一样。有的库会自动补有的库会严格报错还有的库尤其是一些老实现在长度不是4的倍数时会截断或者补错位置。解决办法是统一在拼接签名输入之前先对header和payload的Base64字符串做规范化——确保它们是原始token里的那一段不要自己重新编码一遍。因为重新编码可能会改变填充状态而签名验的是原始字符串的字节不是解码再编码后的结果。提示调试这类问题时最有效的做法是把签名输入的字节序列打印出来用 hex 或者 repr和服务端日志里的做逐字节比对。别靠肉眼看字符串Base64 的差异可能只在一个字符上。6.2 字典顺序比字典大小更重要我一开始的直觉是字典越大命中率越高于是一次性加载了几百万行的字典结果跑得又慢、命中率也没提升多少。后来才想明白爆破的效率取决于命中项在字典里的位置而不是字典的总行数。如果你的字典是随机顺序的命中项可能在第两百万行那前两百万次计算就白费了。反之如果按人类常用度排序命中项排在前几千行几秒钟就出结果。所以字典的正确构造方式是分层的第一层是最常见的几百个弱口令和默认值第二层是项目相关的自定义词项目名、域名、年份的变形第三层才是通用大字典。三层按顺序拼起来命中率高的排前面。这个道理在别的爆破场景里也通用——顺序永远比规模重要。6.3 并发爆破把靶场打挂之后的教训我在自己的靶机上做过一次高并发验证的测试开了一百个进程同时跑Python脚本结果靶机的CPU直接满载服务响应变成超时。虽然那是自己的环境但也提醒了我一件事爆破脚本的资源消耗要提前评估。几个实践上的建议本地爆破纯离线算签名用GPU工具别用多进程Python因为Python的GIL会让多进程的开销大到不划算。如果真的要用多进程进程数不要超过CPU物理核心数超了只会增加上下文切换的开销。涉及在线验证比如爆破之后的token有效性测试时一定要加延迟和限速否则很容易触发对方的防护告警。注意对非自有系统做任何形式的并发请求都要事先确认授权范围包括请求频率和总量。很多授权书里会明确写禁止影响业务可用性超出这个范围就是越界。6.4 只在某些请求上验证失败的排查链路最后一个坑比较隐蔽。现象是同一个token调A接口正常调B接口返回401重启服务之后有时候又反过来。这种间歇性失败基本可以排除密钥本身的问题——如果密钥错了应该是全部失败。排查思路我一般按这个顺序走第一步确认两个接口是不是用了不同的验证中间件。微服务架构下A服务可能用了库XB服务用了库Y两个库对同一段token的解析方式可能有细微差异比如对exp的容忍度、对aud的校验严格程度。第二步确认两边的时间是否同步。exp和nbf是时间敏感的如果服务器之间有几分钟的时钟漂移就会出现这台机器认为没过期、那台认为过期了的情况。第三步抓包看两个接口收到的token字节是否完全一致。有些网关或者代理会改写请求头比如对Authorization字段做某种处理改了之后签名当然对不上。第四步检查密钥版本。如果密钥做了轮换但只有部分服务更新了配置就会出现新token在老服务上验不过的情况。这条链路排查下来绝大多数玄学失败都能定位到具体原因。核心思路是把验证失败拆成算法对不对、密钥对不对、字段对不对、时间对不对、字节对不对这五个独立的问题逐个排除。回到开头那个场景拿到一个JWT先解码看alg判断这个token是不是对称算法、是不是可爆然后看header里有没有kid、jku这些参数试成本最低的几条路径最后才是字典和算力。站在防守方把密钥换成openssl rand -base64 32的输出、把算法白名单写死、把声明字段校验补齐这三件事做完爆破这条路基本就被堵死了。我个人的体会是JWT的安全问题很少出在算法设计上绝大多数都是实现时的偷懒——图省事用了默认密钥、图方便从header读算法、图快速跳过了声明校验。这些偷懒在功能测试阶段完全看不出来只有被人拿字典跑一遍才会暴露。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询