JWT看着像一串乱七八糟的字符,但拆开来看其实特别规整。它由三小节组成,中间用点隔开,像“xxx.yyy.zzz”这样。很多朋友天天用JWT,但从来没仔细看过最前面那一截“xxx”到底代表什么。今天咱们就专门把这块翻出来聊透,看看里面藏着哪些关键参数,以及它们是怎么变成漏洞入口的。
一、先看看JWT长什么样
1.1 以点分割的三段式
一个标准的JWT通常长这个样子:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEsInJvbGUiOiJhZG1pbiJ9.tjW9zT_7nU9jHZtTboMIjFxD_eCWOe0BdCQK7BGmB1w
你可以把它理解成三截:第一截是Header,第二截是Payload,第三截是签名。每一截都是经过Base64编码的JSON字符串。点号的作用就是告诉程序“该分开了”。平时我们登录后把整个token放在请求的Authorization头里,后端拿到手会先按点拆开,再分别处理。
1.2 为什么Header放在最前面
Header就像一包方便面外面的配料表,它不负责提供营养,但告诉你这包面是什么口味、该用什么温度的水泡。同样,JWT的Header声明了本次令牌使用的签名算法和类型,让验签方知道“该用什么姿势去验证”。放在最前面,就是为了让解析器第一时间看到元数据,不用翻到后面去猜。
二、Header里有哪些关键参数
2.1 alg:算法那个“大当家”
alg是Header里最重要的参数,它的全称是“Algorithm”,也就是签名算法。常见的值有HS256(对称密钥)、RS256(非对称密钥)、ES256(椭圆曲线)等等。有时候还会看到none,这表示不签名。你可以简单理解成:
alg=HS256 // 我和对方用同一个密码,验证时用这密码算一遍
alg=RS256 // 我用私钥签名,大家用公钥验签
alg=none // 我不签名了,你爱信不信
这里有个非常关键的认知:alg是放在Header里的,而Header是前端用户可以改的。如果后端不校验alg的值,光看着Header里写了什么就去用对应算法验签,那等于把验签规则的主导权交给了对方。
2.2 typ和cty:类型与内容类型
typ一般固定是JWT,表示这是一个JWT格式的令牌。它有点像你发邮件时填的“主题”,只是给解析器一个提示,实际用途不大。但也有极少数库会读取typ来决定解析方式,所以也不能乱写。
cty是“Content Type”的缩写,在JWT里不常用,但如果出现了,它可能表示Payload里的内容格式,例如application/json。这玩意儿在普通API认证中几乎用不到,但在特殊的嵌套JWT场景里才有意义。
2.3 kid:密钥的“门牌号”
kid是“Key ID”的缩写,它的作用是告诉服务端“该用哪把钥匙来验签”。就像你进酒店房间,门牌号告诉你该用哪把钥匙开门。但是,如果服务端把kid当作查询参数去拼SQL或拼文件路径,那就会变成注入漏洞。后面咱们在安全隐患里细说。
三、Node.js实战:解析Header信息
3.1 生成一个带Header的JWT
我们准备用Node.js里的jsonwebtoken库来做实验。先用它顺手造一个令牌,然后在控制台里把它的Header打印出来。
// 技术栈:Node.js + jsonwebtoken
const jwt = require('jsonwebtoken');
// 定义一个简单的密钥,实际生产环境可别用这么短的
const SECRET = 'my-super-secret-key';
// 要放到令牌里的用户信息
const payload = {
userId: 123,
role: 'admin'
};
// 生成JWT,指定用HS256算法,并且设置Header中的typ为JWT
const token = jwt.sign(payload, SECRET, {
algorithm: 'HS256',
header: { typ: 'JWT' }
});
console.log('生成的JWT完整字符串:');
console.log(token);
运行上面的代码,你会看到一个三段的字符串输出。拿第一段去Base64解码,就能看到Header的原始JSON内容。
3.2 手动解码Header
接下来我们不需要依赖库,自己动手把Header部分解码出来,这样你能更清楚地看到它是怎么被编码的。
// 技术栈:Node.js(不需要外部库,纯手写解码逻辑)
const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEsInJvbGUiOiJhZG1pbiJ9.tjW9zT_7nU9jHZtTboMIjFxD_eCWOe0BdCQK7BGmB1w';
// 按点号切割token
const parts = token.split('.');
const headerBase64 = parts[0]; // 拿到第一段
console.log('Header的Base64Url:', headerBase64);
// Node.js的Buffer支持base64url直接解码
const headerBuffer = Buffer.from(headerBase64, 'base64url');
const headerJson = headerBuffer.toString('utf-8');
console.log('解码后的Header JSON:', headerJson);
// 执行结果应输出:{"alg":"HS256","typ":"JWT"}
这样你就亲眼看到了Header的内容:alg是HS256,typ是JWT。手动解码能帮助你理解为什么我们说“Header是可篡改的”——因为它只是Base64编码,不是加密,任何人都能解码甚至改内容再重新编码。
3.3 用jwt.decode快速查看
如果你不想手写解码,jsonwebtoken库自带的decode方法也能直接看到Header和Payload,而且不会校验签名,很安全。
// 技术栈:Node.js + jsonwebtoken
const jwt = require('jsonwebtoken');
// 随便一个JWT字符串
const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEsInJvbGUiOiJhZG1pbiJ9.tjW9zT_7nU9jHZtTboMIjFxD_eCWOe0BdCQK7BGmB1w';
// 传入complete:true,返回的对象会同时包含header和payload
const decoded = jwt.decode(token, { complete: true });
console.log('Header:', decoded.header);
console.log('Payload:', decoded.payload);
console.log('签名原文:', decoded.signature);
decode方法纯粹是把JWT拆开给你看,不做任何安全校验,所以用于调试和排查非常方便。但也要注意,不能把它当“验证”来用。
四、安全隐患大排查
4.1 alg:none,零签名大盗
这是最经典的JWT攻击。如果服务端在验证签名时,没有对alg参数做白名单限制,攻击者可以把Header里的alg改成none,然后去掉签名部分,就能伪造任意token。
比如攻击者构造一个假的token:
// 技术栈:Node.js
const jwt = require('jsonwebtoken');
// 伪造的payload,想当管理员
const forgedPayload = { userId: 1, role: 'admin' };
// 使用alg:'none'生成token(注意jsonwebtoken库默认禁止none,这里为了演示所以临时设置)
try {
const fakeToken = jwt.sign(forgedPayload, '', {
algorithm: 'none',
header: { typ: 'JWT' }
});
console.log('伪造的token:', fakeToken);
} catch (err) {
console.log('库禁止了none,但很多旧库或自研代码不会:', err.message);
}
如果后端用的是一个更宽松的库,或者自己写了签名校验逻辑,只检查了这个token有没有签名,但没有检查签名算法是不是none,那这个伪造token就能直接通过验证。解决办法很简单:服务端在验证的时候,必须强制声明允许的算法列表,比如只允许HS256或者RS256,遇到none直接拒绝。
// 技术栈:Node.js + jsonwebtoken
// 安全验证示例:明确限定只接受HS256
const jwt = require('jsonwebtoken');
const SECRET = 'my-super-secret-key';
const forgedToken = 'eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJ1c2VySWQiOjEsInJvbGUiOiJhZG1pbiJ9.';
try {
// 当algorithms里只写HS256时,alg为none的token会直接报错
const decoded = jwt.verify(forgedToken, SECRET, { algorithms: ['HS256'] });
console.log('验证成功,这个令牌有效', decoded);
} catch (err) {
console.log('验证失败,幸好我们限制了算法:', err.message);
}
4.2 算法混淆攻击(RS256/HS256)
这个坑稍微隐蔽一点。假设你的服务端本打算使用RSA非对称签名(RS256),验证时会用公钥。但攻击者把Header里的alg改成HS256,然后用服务端的公钥作为HMAC的密钥来签名。如果服务端验证时没有限定算法,就会用同一个“密钥”去跑HS256,结果误以为这个token是合法的。
可以这样复现:
// 技术栈:Node.js + jsonwebtoken + crypto
const jwt = require('jsonwebtoken');
const crypto = require('crypto');
const fs = require('fs');
// 模拟服务端持有的RSA公钥
// 注意:公钥文件的内容是公开的,攻击者也能拿到
const publicKey = fs.readFileSync('./public.pem', 'utf8');
// 攻击者要伪造的payload
const forgedPayload = { userId: 1, role: 'admin' };
// 关键:把签名算法改成HS256,并且签名密钥用公钥内容
const forgedToken = jwt.sign(forgedPayload, publicKey, {
algorithm: 'HS256',
header: { alg: 'HS256', typ: 'JWT' }
});
console.log('使用公钥作为HMAC密钥生成的伪造token:', forgedToken);
// 服务端如果只用公钥验证,而不限制算法,就会中招
try {
// 这里没有指定algorithms,默认会尝试根据header来选算法
const decoded = jwt.verify(forgedToken, publicKey);
console.log('糟糕!验证通过了:', decoded);
} catch (err) {
console.log('验证失败:', err.message);
}
要防止这个漏洞,服务端在配置jsonwebtoken时一定要传入algorithms数组,明确指定允许的算法。
// 技术栈:Node.js + jsonwebtoken
// 安全加固:固定算法
const jwt = require('jsonwebtoken');
try {
// algorithms只写RS256,任何HS256的token都会报错
const decoded = jwt.verify(forgedToken, publicKey, { algorithms: ['RS256'] });
console.log('验证通过', decoded);
} catch (err) {
console.log('正确拦截了算法混淆攻击:', err.message);
}
4.3 kid参数注入风险
kid这个参数虽然不像alg那么致命,但很危险。因为它经常被服务端当作某个密钥文件的索引。比如:
kid = "server1" -> 对应 public_key_server1.pem
kid = "server2" -> 对应 public_key_server2.pem
如果服务端把kid拼进SQL查询或文件路径里,攻击者就可以通过kid做SQL注入或路径穿越。例如:
"kid": "../../etc/passwd"
或者:
"kid": "x' UNION SELECT 'xx'"
这些攻击的核心原因是:服务端过于信任Header里的kid,没有把它当成“不可信输入”来处理。因此,务必对kid做白名单校验,或者限制只能匹配已知集合。
4.4 Header里的敏感信息泄漏
有人会把一些无关信息塞进Header里,比如自定义参数,但这样做很容易造成信息泄漏。尤其是有时候开发图省事,把密钥版本、内部环境名、甚至明文密码放在Header里。这种操作等于把秘密写在明信片上寄出去。记住,Header只是Base64编码,任何人都可以解码。不要在JWT Header中存放任何敏感信息,哪怕它不会发送到客户端。
五、怎么避坑:最佳实践与排查清单
5.1 服务端必须设置算法白名单
这是所有防护里最重要的一条。无论你是用jsonwebtoken、jjwt,还是别的库,验签时都要显式指定allowedAlgorithms。这样即使有人篡改Header中的alg,你的验签函数也会直接拒绝。
5.2 密钥管理要上强度
不要使用弱密钥,比如secret、123456这种。同时,非对称密钥的私钥要放在安全的配置中心或KMS里。对称密钥的密钥长度也要够长,否则容易被暴力破解。很多攻击工具会用常见弱密钥表去爆破HS256的签名。
5.3 不要信任Header里的任何“可读”内容
Header里的kid、alg、typ都是输入数据,不是安全配置。它们只能提示,不能决定安全策略。安全策略必须由服务端硬编码。比如你想用哪个密钥,应该根据业务域名或用户来源去查,而不是直接拿Header里的kid去找文件。
5.4 排查问题时的检查表
如果你正在排查为什么JWT验证出了问题,可以先按这个表自检:
1. 是否在验签时指定了algorithms?
2. 是否允许alg:none?
3. 是否用公钥内容当作HS256的密钥?
4. 是否把kid参数用于拼SQL/文件路径?
5. Header里是否出现了非JWT标准的自定义参数?
6. 密钥是否强度足够?
只要有一项答案是“是”,系统大概率存在风险。
六、应用场景与优缺点
6.1 适合用JWT的场景
JWT非常适合处理无状态API认证。比如客户端登录后拿到一个token,之后每个请求都带上它,服务端不需要保存会话信息,直接验签就行。这对分布式系统、微服务架构特别友好,因为任何服务节点只要拿到同样的密钥或公钥,都能独立完成验证。
6.2 JWT的优缺点
优点也很明显:结构简单、自包含、移动端和Web端通用、便于横向扩展。但缺点同样需要重视:Header和Payload只是Base64编码,没有加密,敏感信息不能放进去;token一旦签发,在到期前无法轻易作废,除非额外加黑名单;如果密钥泄漏,所有token都面临被伪造的风险。
七、总结
JWT的Header虽然只是一段简单的元数据,但里面的alg、kid等参数,直接决定了签名验证的流程是否安全。很多严重的JWT漏洞,根本不是因为加密算法被破解,而是因为服务端盲目相信了Header里写的“提示”。所以,我们要把Header当成“用户输入”来看待,而不是“系统配置”。只要对算法做白名单、对kid做校验、对密钥做强化,JWT依然是一个快速有效的身份认证方案。
评论
围绕“深入解析JWT的Header部分:关键参数与安全隐患排查”参与讨论