背景
相信很多人在使用 postman 调用接口时都遇到了这样的问题,就是请求的网站需要验证 JWT,而我们虽然可以一键生成,但每次生成后都要重新粘到 postman 的请求头中才会生效,这不免带来许多麻烦,更头疼的是,大部分 JWT 的有效时间只有 10 分钟,当我们进行其他工作,再回过神来调用接口时,又会抛出令人讨厌的 403 错误,迫使我们再次手动生成,我们需要解决这个问题。
什么是 JWT
JWT 全称 Json Web Token,字面意思就是 Json 对象在网络中传输用到的令牌,而令牌的作用就是确保传输过程中的安全性。
授权:这是使用 JWT 的最常见场景。用户登录后,每个后续请求都将包含 JWT,允许用户访问该令牌允许的路由、服务和资源。单点登录是当今广泛使用 JWT 的一项功能,因为它的开销小,并且能够轻松地跨不同域使用。
信息交换:JSON Web 令牌是在各方之间安全地传输信息的好方法。由于 JWT 可以签名(例如,使用公钥/私钥对),因此您可以确保发件人是他们所声称的身份。此外,由于签名是使用标头和有效负载计算的,因此您还可以验证内容是否未被篡改。
可以使用密钥(使用 HMAC 算法)或使用 RSA 或 ECDSA 的公钥/私钥对对 JWT 进行签名。
官网地址:https://jwt.io/introduction
JWT 结构
JWT 一般由三个部分组成:
- header
- payload
- signature
而由他们三个生成的JWT格式一般是xxxxx.yyyyy.zzzzz
,中间由 . 进行分隔,下面是一个真实的 JWT:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhY2Nlc3NLZXlJZCI6ImlkYWFzbW5nXzAzMWYxMDc2OGE2ZjRhNGE5M2FiZWU2MDViYjgzNzg1IiwiZXhwIjoxNzMzNzkyMDkwfQ.AD_cguC8JPS9HIMHz0y0yWU--UYpgKnKRkd_SDpDSYU
header
header通常由两部分组成:令牌的类型(JWT)和正在使用的签名算法,例如 HMAC SHA256 或 RSA。
例如:
{
"alg": "HS256",
"typ": "JWT"
}
然后,此 JSON 经过 Base64Url 编码以形成 JWT 的第一部分。也就是xxxxx
这一部分。
通常这一部分是保持不变的,因为这一部分信息并不涉及什么重要的安全信息,只是告知后台应当采用什么算法进行加密验签。
payload
JWT 的第二部分是payload,其中包含声明。声明是关于实体(通常是用户)和其他数据的声明。 有三种类型的声明:已注册、公共和私有声明。
- 已注册的声明 Register Claims:这些是一组预定义的声明,不是强制性的,但建议使用,以提供一组有用的、可互操作的声明。其中一些是:iss(颁发者)、exp(过期时间)、sub(主题)、aud(受众)等。
请注意,声明名称只有三个字符长,因为 JWT 是紧凑的。
我们一般会携带 exp,因为 exp 的值是当前时间加上有效期的一个时间类型值,所以每次经过Base64加密生成的结果都不一样。
-
公共声明 Public Claims:这些声明可以由使用 JWT 的用户随意定义。但为避免冲突,应在 IANA JSON Web 令牌注册表中定义它们,或将其定义为包含抗冲突命名空间的 URI。
-
私有声明 Private Claims:这些是自定义声明,用于在同意使用它们的各方之间共享信息,既不是注册声明,也不是公开声明。
一个有效的payload示例:
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
然后,对该 JSON 进行 Base64Url 编码,以形成 JWT 的第二部分。也就是yyyyy
这一部分。
请注意,对于签名令牌,此信息虽然可以防止篡改,但任何人都可以读取。除非 JWT 已加密,否则不要将机密信息放在 JWT 的 payload 或 header 元素中。
signature
要创建signature部分,您必须获取编码的header、编码的payload、密钥、标头中指定的算法,并对其进行签名。
例如,如果您想使用 HMAC SHA256 算法,将按以下方式创建signature:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
最后,再次对HMAC SHA256 算法加密后得到的byte数组进行base64加密,就得到了 JWT 的第三部分。也就是zzzzz
这一部分。
signature用于验证消息在整个过程中没有被更改,并且在使用私钥secret
签名的令牌的情况下,它还可以验证 JWT 的发件人是否是它所声称的身份。
我们要怎么做
postman 中有这样一个选项 Scripts ,可以在里面编写 JavaScript 代码,左边的 pre-request 和 post-response 则是指定在请求前的操作和接收到请求结果后的操作。( PS:旧版 postman 的 Scripts 应该是 pre-request )
我们要做的,就是在这里面生成所需的 JWT ,并且放入 postman 的环境变量中,这样,我们的接口在调用时,只需要使用双大括号{{}}的的方式,就可以自动从环境变量取到所需的 JWT 。
还记得上文提到的一个真实的 JWT 吗?让我们来分析一下。
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhY2Nlc3NLZXlJZCI6ImlkYWFzbW5nXzAzMWYxMDc2OGE2ZjRhNGE5M2FiZWU2MDViYjgzNzg1IiwiZXhwIjoxNzMzNzkyMDkwfQ.AD_cguC8JPS9HIMHz0y0yWU--UYpgKnKRkd_SDpDSYU
第三部分AD_cguC8JPS9HIMHz0y0yWU--UYpgKnKRkd_SDpDSYU
好像和前两个部分不太一样。
它携带了下划线 _ 和减号 - 这些字符,这是因为先经过HMAC SHA256加密再经过 base64 编码后可能会产生加号 + ,等于号 = ,斜杠 / 这些不安全符号,经过转义后就会产生第三部分的结果。
现在我们已经知道了原理,进入编码部分。
首先,引入我们所需的类。其中 btoa 是一个安全的 base64 算法。
const CryptoJS = require('crypto-js')
const btoa = require('btoa')
完成 JWT 第一部分xxxxx
,得到eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
let headerJson = JSON.stringify({
"alg": "HS256",
"typ": "JWT"
})
let header = btoa(headerJson)
注意:JSON 内属性的顺序改变也会改变header
完成 JWT 第二部分yyyyy
,得到eyJhY2Nlc3NLZXlJZCI6ImlkYWFzbW5nXzAzMWYxMDc2OGE2ZjRhNGE5M2FiZWU2MDViYjgzNzg1IiwiZXhwIjoxNzMzNzkyMDkwfQ
// 超时时间为10分钟
let exp = new Date().getTime() + 10*60*1000
let ak = pm.environment.get("ak")
let payloadJson = JSON.stringify({
"accessKeyId": ak,
"exp": exp,
})
let payload = btoa(payloadJson);
这边我使用到了aksk,其中公钥ak放在payload内,私钥sk放在第三部分用于HMAC SHA256加密
完成 JWT 第三部分zzzzz
,得到AD_cguC8JPS9HIMHz0y0yWU--UYpgKnKRkd_SDpDSYU
function safeEncode(header,payload,sk) {
// HmacSHA256加密
let hash = CryptoJS.HmacSHA256(header+"."+payload,sk)
// base64加密
let base64 = CryptoJS.enc.Base64.stringify(hash);
let reg = new RegExp("/", "g");
// 手动替换为可以被校验的符号
base64 = base64.replace(/=+/,"").replace(/\+/g,"-").replace(reg,"_");
return base64;
}
let signature=safeEncode(header,payload,sk)
最后的最后,就是拼接起来,并放到环境变量中,就得到了每次都是随机生成的 JWT,大功告成!
let jwt=header + "." + payload+"."+signature
pm.environment.set("SSO-JWT-Authorization",jwt)
完整代码如下:
const CryptoJS = require('crypto-js')
const btoa = require('btoa') // 安全的base64加密算法
// 通过header,payload,sk生成签名signature
function safeEncode(header,payload,sk) {
// HmacSHA256加密
let hash = CryptoJS.HmacSHA256(header+"."+payload,sk)
// base64加密
let base64 = CryptoJS.enc.Base64.stringify(hash);
let reg = new RegExp("/", "g");
// 手动替换为可以被校验的符号,
base64 = base64.replace(/=+/,"").replace(/\+/g,"-").replace(reg,"_");
return base64;
}
function generateToken() {
let ak=pm.environment.get("ak")// 放在aksk环境变量里,可以自行调整
let sk=pm.environment.get("sk")
let iat = new Date().getTime()
// 超时时间为10分钟
let exp = iat + 10*60*1000
let headerJson = JSON.stringify({
"typ": "JWT",
"alg": "HS256"
})
let payloadJson=JSON.stringify({
"accessKeyId": ak,
"exp": exp,
})
let header=btoa(headerJson)
let payload=btoa(payloadJson)
let signature=safeEncode(header,payload,sk)
// 拼接得到jwt
let jwt=header + "." + payload+"."+signature
pm.environment.set("SSO-JWT-Authorization",jwt)
console.log(jwt)
}
generateToken()