JWT(JSON Web Token)
JWT 它是一个字符串,表示一组字段声明的集合 JSON 并以格式组织数据 JWS 或 JWE 方式编码。
JWT 由 Header、Payload、Signature 三部分由英语组成,三部分用英语 . 分隔。
JWTString = Base64(Header) "." Base64(Payload) "." Signature
Header
JWT标头,是一种描述JWT元数据的JSON对象。
| 字段名 |
类型 |
说明 |
| alg |
String |
使用签名算法。HMAC SHA256缩写为 HS256 |
| typ |
String |
声明 JWT 的媒体类型(IANA.MediaTypes)。固定赋值一般为 |
{ "alg": "HS256", "typ": "JWT" }
Payload
JWT荷载,也是一个JSON对象包含需要传输的数据。载荷字段有三种声明方式:
-
- Registered Claims
- Public Claims
- Prigate Claims
Registered Claims
即RFC7519中规定的JWT注册声明字段不是强制性要求JWT但它提供了一些字段JWT共有字段。注册声明的字段名很短,因为JWT的特征之一是紧凑的数据格式。
| 字段名 |
类型 |
说明 |
| iss |
发行人可填写应用标志 |
|
| exp |
到期时间是时间戳 |
|
| sub |
主题,JWT面向的用户 |
|
| aud |
用户,JWT的接收方 |
|
| nbf |
在此时间前JWT不可用 |
|
| iat |
发布时间,时间戳 |
|
| jti |
JWT ID,即JWT的标识ID |
Public Claims
由使用JWT为了防止命名冲突,需要向组织定义 IANA JSON Web Token Registry 注册字段的定义符合其命名规则。
Private Claims
私人声明是由的JWT发布人与接收人约定的字段,非公开声明的字段。
Signature
JWT 签名。对 Header 和 Payload 的 Base64编码值 使用加密算法计算后,获得签名。计算签名时使用的密钥(secret)被称为 。
Signature 部分是可选的。如果一个 JWT 无 Signature 部分被称为Unsecured JWT”。无 Signature 部分时,标头alg字段应声明值为none。
以 HMAC SHA256 以加密算法为例,签名的计算公式为:
Signature = HMACSHA256(Base64(header) "." Base64(payload), secret)
示例
一个 Unsecured JWT 示例如下:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2F1dGguYnJvemVuLnRvcCIsImF1ZCI6IldlQ2hhdEFwcCIsInN1YiI6IjEyMzEyMyIsImlhdCI6MTY1NTg5MzYzMiwibmJmIjoxNjU1ODkzNjMyLCJqdGkiOiJ0ZXN0U2Vzc2lvbklkIiwiZ3JvdXBzIjpbIldlQ2hhdEFwcCJdLCJub25jZSI6InRlc3RTZXNzaW9uSWQiLCJleHAiOjE2NTU5ODAwMzJ9
Header 部分解码为(已格式化):
{ "alg": "HS256", "typ": "JWT" }
这里直接用于演示。 JWS 移除了 Signature 部分,因此 Header 部分有签名算法 alg 字段。
Payload 部分解码为(已格式化):
{ "iss": "https://auth.brozen.top", "aud": "WeChatApp", "sub": "123123", "iat": 1655893632, "nbf": 1655893632, "jti": "testSessionId", "groups": ["WeChatApp"], "nonce": "testSessionId", "exp": 1655980032 }
JWS(JSON Web Signature)
JWS 是在 Unsecured JWT 基础上,Header 部分声明签名算法并添加 Signature 部分。
JWS 通过添加签名,接收方可以验证 JWT 有效性,签名理由 Payload 加密算法计算得到。如果接收方使用通用算法和密钥对 Header 和 Payload 加密,得到的密文与 Signature 部分不同,说明 Payload 被篡改。
JWS 包括结构和结构 Signature 部分的 JWT 相同。
Header
JWS 的标头除了 JWT 的标头alg和typ之外,还有一些扩展标头:
| 字段名 |
类型 |
说明 |
| alg |
String |
签名使用的算法,如:HMAC SHA256缩写为 HS256。 RFC7518 中规定的合法加密算法参考此处。 |
| typ |
String |
令牌类型,统一固定为"JWT"。 |
| jku |
String |
JWK Set URL,是一个URI,用于获取 JWK 集合。此 URL 需要在传输层提供安全保护(TLS、HTTPS)。此标头非必填。 |
| jwk |
String |
是一个 JWK,是用于签名此 JWS 的加密公钥。此标头非必填。 |
| kid |
String |
Key ID,是一个大小写敏感字符串,JWK 的发布者给接受者的提示信息,用于表明加密此 JWS 使用的 Key。此标头非必填。可配合 jwk 标头使用。 |
| x5u |
String |
是一个URI,指向加密此 JWS 的 X.509数字证书或证书链。此标头非必填。 |
| x5c |
String |
是加密此 JWS 的 X.509数字证书或证书链。此标头非必填。 |
| x5t |
String |
是加密此 JWS 的 X.509数字证书的 SHA-1 指纹,Base64格式。此标头非必填。 |
| x5t#S256 |
String |
是加密此 JWS 的 X.509数字证书的 SHA-256 指纹,Base64格式。此标头非必填。 |
| cty |
String |
是此 JWS 的荷载部分的媒体类型,可能不会被使用。此标头非必填。 |
| crit |
String[] |
是一组 JWS 标头名,接受此 JWS 的应用必须理解并能够处理这组标头,如存在无法处理的标头,应用应当认为此 JWS 是非法的。此列表中的标头不可以是上述的标准头(可以是公有头或私有头)。此标头非必填。 |
验签
使用 JWS 的目的是为了保护 JWT 不被篡改,因此 JWT 的接收方需要验证 Signature 的有效性,此过程称为验签。
对称加密算法加密的 Signature,发布方使用私钥进行签名,接收方可以使用公钥校验 Header 与 Payload(解密 Signature 校验)。
非对称算法要求接收方拥有与 JWS 发布方相同的密钥,接收方使用相同的密钥校验 Header 与 Payload(加密 Payload 校验)。
示例
一个合法 JWS 的示例如下:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2F1dGguYnJvemVuLnRvcCIsImF1ZCI6IldlQ2hhdEFwcCIsInN1YiI6IjEyMzEyMyIsImlhdCI6MTY1NTg5MzYzMiwibmJmIjoxNjU1ODkzNjMyLCJqdGkiOiJ0ZXN0U2Vzc2lvbklkIiwiZ3JvdXBzIjpbIldlQ2hhdEFwcCJdLCJub25jZSI6InRlc3RTZXNzaW9uSWQiLCJleHAiOjE2NTU5ODAwMzJ9.t5Gqvyz6GNHsVPlnJ3Oq7vNH0T3E7MNvTj_aDcgI9lE
Header 与 Payload 部分和 JWT 示例相同,签名使用 HS256 算法,密钥为:secretddsecretddsecretddsecretdd。
JWE(JSON Web Encryption)
JWS 中,JWT Payload 部分仍是明文,第三方获取到之后可以直接查看。JWE 的目的是对 JWT 整体进行加密,防止第三方截获解析 JWT。
JWE 的目的是在 JWT 发布方加密 JWT、在 接收方解密 JWT,因此 JWE 必须采用对称加密算法。同时由于对称加密的存在,JWE 的接收方可以在 JWT 中新增 Claims,这是 JWS 不支持的。
JWE 由 Header、加密密钥、加密算法的IV、密文、算法附加数据组成,格式如下:
JWEString = Base64(Header) + "." + Base64(Encrypted Key) + "." + Base64(Initialization Vector) + "." + Base64(Encrypted Data) + "." + Base64(Auth Tag)
JWE 结构
Header
JWE 的 Header 与 JWS 大部分相同,仅有如下区别,其他头部 jku / jkw / kid / x5u / x5c / x5t / x5t#S256 / typ / cty / crit 与 JWS 定义完全一致。
| 字段名 |
类型 |
说明 |
| typ |
String |
令牌类型,统一固定为"JWT"。 |
| alg |
String |
加密密钥使用的算法,改算法用于对“加密数据的密钥”进行加密。 RFC7518 中规定的合法加密算法有:
|
| enc |
String |
加密数据的算法名称。RFC7518 中规定的合法加密算法有:
|
| zip |
String |
加密前的数据压缩算法,可以不填。 |
Encrypted Key
这里是对数据加密的密钥,经过 alg 算法处理后的密文。也即一个 JWE 中至少存在两种加密算法,分别用于:对数据加密、对加密数据的密钥加密。
对加密数据的密钥进行加密后,需要使用 JWK 密钥管理模式来导出密钥。
Initialization Vector
部分加密算法需要额外数据,或随机数据,在此字段中存储。
Encrypted Data
数据被加密后的密文。
Authentication Tag
认证标记,数据加密算法产生的附加数据,用于防止密文被篡改。
由于数据比较大,使用带有公私钥密钥对的对称加密算法(如 RSA)加密会很慢,因此往往采用 AES-GCM(Galois/Counter Model) 或 AES_CBC_HMAC_SHA 算法,此种算法只有一个加密密钥,但加解密速度快。因此使用 AES 单密钥算法对数据加密,然后使用 RSA 对 AES 密钥加密,既保证了安全性(数据被加密、AES密钥不会泄漏),又保障了加密速度(AES 加密数据快,RSA 仅加密密钥也比较快)。
JWK(Json Web Key)
JWK 是一个 JSON 格式的对象,表示加密用的密钥。JWK 中的字段表示密钥的相关属性。
JWKs 是一个 JSON 格式的对象,有一个 JWK 数组字段,数组中每个元素的字段与 JWK 相同,表示 JWK 的集合。
JWK 格式
| 字段名 |
类型 |
说明 |
| kty |
String |
密钥生成时使用的加密算法,大小写敏感。取值定义与 JWA,如“RSA”、“EC”。必填项。 |
| use |
String |
公钥的使用目的,用于加密数据或验证数据签名。取值有:
必填项。 |
| key_ops |
String[] |
表示使用此密钥的操作,取值有:
必填项。 |
| alg |
String |
使用此密钥的加密算法。如“RS256”、“ES256”。必填项。 |
| kid |
String |
密钥ID,用于在 JWK 集合中匹配单个密钥。 |
| n |
公钥模值。 |
|
| e |
公钥指数。 |
|
| x5u |
String |
是一个URI,指向加密此 JWS 的 X.509数字证书或证书链。 |
| x5c |
String |
是加密此 JWS 的 X.509数字证书或证书链。 |
| x5t |
String |
是加密此 JWS 的 X.509数字证书的 SHA-1 指纹,Base64格式。 |
| x5t#S256 |
String |
是加密此 JWS 的 X.509数字证书的 SHA-256 指纹,Base64格式。此标头非必填。 |
例如,一个 kid 为 "sign-key" 的 HS256 算法 JWK 文件格式如下:
{
"kty": "oct",
"kid": "sign-key",
"use": "sig",
"key_ops": ["sign", "verify"],
"alg": "HS256",
"k": "RqVfW84rHBqwabFnZaRh_td19WvQT0x1b1u_MxsEyRs"
}
JWK Set 格式
| 字段名 |
类型 |
说明 |
| keys |
JWK[] |
JWK 密钥。 |
例如,包含两个 JWK 的 JWK Set 文件格式如下,包含两个 HS256 算法密钥, kid 为 "sing-key" 和 "content-enc-key"。
{
"keys": [{
"kty": "oct",
"kid": "sign-key",
"use": "sig",
"key_ops": ["sign", "verify"],
"alg": "HS256",
"k": "RqVfW84rHBqwabFnZaRh_td19WvQT0x1b1u_MxsEyRs"
},
{
"kty": "oct",
"kid": "content-enc-key",
"use": "sig",
"key_ops": ["sign", "verify"],
"alg": "HS256",
"k": "PwVsCTlHKSwmZWYF2yXXSBc0mkuqecyOXY5o7M9iKR0"
}
]
}
JWK 生成
以 jose4j 库为例,介绍 JWK 如何生成。下面以 HMAC、RSA 两种常见算法为例介绍。
HMAC
OctetSequenceJsonWebKey key = OctJwkGenerator.generateJwk(256);
key.setKeyId("sign-key");
key.setAlgorithm("HS256");
key.setUse("sig");
key.setKeyOps(List.of("sign", "verify"));
key.setOtherParameter("kty", "oct");
System.out.println(key.toJson());
RSA
这里使用的是 RSA-OAEP-256 算法。注意 Java 中只能使用 PKCS#8 格式私钥,如果从 PEM 文件中读取到 PKCS#1 格式,需要使用加密库完成私钥格式转换,例如 Bouncy Castle。
public JsonWebKey createRSAKeys(String keyId) {
try {
// 生成私钥,或者从文件读取私钥皆可。
KeyPairGenerator kpGenerator = KeyPairGenerator.getInstance("RSA");
kpGenerator.initialize(2048);
KeyPair kp = kpGenerator.generateKeyPair();
PrivateKey prvKey = kp.getPrivate();
// 生成 JWK
RsaJsonWebKey key = RsaJwkGenerator.generateJwk(2048);
key.setKeyId(keyId);
key.setAlgorithm("RSA-OAEP-256");
key.setUse("enc");
key.setKeyOps(List.of("encrypt", "decrypt", "warpKey", "unwarpKey", "encryption"));
key.setOtherParameter("kty", "RSA");
key.setPrivateKey(prvKey);
return key;
} catch (Exception e) {
log.error("生成 RSA JWK 失败", e);
throw new IllegalStateException("生成 RSA JWK 失败", e);
}
}
JWA(Json Web Algorithm)
JWA 是 RFC7581 中规定的算法,用于 JWT 的签名或加密。
JWS 签名算法
| "alg" 标头取值 |
算法描述 |
| HS256 |
HMAC using SHA-256 |
| HS384 |
HMAC using SHA-384 |
| HS512 |
HMAC using SHA-512 |
| RS256 |
RSASSA-PKCS1-v1_5 using SHA-256 |
| RS384 |
RSASSA-PKCS1-v1_5 using SHA-384 |
| RS512 |
RSASSA-PKCS1-v1_5 using SHA-512 |
| ES256 |
ECDSA using P-256 and SHA-256 |
| ES384 |
ECDSA using P-384 and SHA-384 |
| ES512 |
ECDSA using P-512 and SHA-512 |
| PS256 |
RSASSA-PSS using SHA-256 and MGF1 with SHA-256 |
| PS384 |
RSASSA-PSS using SHA-384 and MGF1 with SHA-384 |
| PS512 |
RSASSA-PSS using SHA-512 and MGF1 with SHA-512 |
JWE Key 加密算法
| "alg" 标头取值 |
算法描述 |
附加 Header |
| RSA1_5 |
RSAES-PKCS1-v1_5 |
|
| RSA-OAEP |
RSAES OAEP |
|
| RSA-OAEP-256 |
RSAES OAEP using SHA-256 and MGF1 with SHA-256 |
|
| A128KW |
AES Key Wrap with value using 128-bit key |
|
| A192KW |
AES Key Wrap with value using 192-bit key |
|
| A256KW |
AES Key Wrap with value using 256-bit key |
|
| ECDH-ES |
Elliptic Curve Diffie-Hellman Ephemeral Static key agreement using Concat KDF |
|
| ECDH-ES+A128KW |
ECDH-ES using Concat KDF and CEK wrapped with A128KW |
|
| ECDH-ES+A192KW |
ECDH-ES using Concat KDF and CEK wrapped with A192KW |
|
| ECDH-ES+A256KW |
ECDH-ES using Concat KDF and CEK wrapped with A256KW |
|
| A128GCMKW |
Key wrapping with AES GCM using 128-bit key |
|
| A192GCMKW |
Key wrapping with AES GCM using 192-bit key |
|
| A256GCMKW |
Key wrapping with AES GCM using 256-bit key |
|
| PBES2-HS256+A128KW |
PBES2 with HMAC SHA-256 and "A128KW" wrapping |
|
| PBES2-HS384+A192KW |
PBES2 with HMAC SHA-384 and "A192KW" wrapping |
|
| PBES2-HS512+A256KW |
PBES2 with HMAC SHA-512 and "A256KW" wrapping |
|
JWE Content 加密算法
| "enc" 标头取值 |
算法描述 |
| A128CBC-HS256 |
AES_128_CBC_HMAC_SHA_256 authenticated encryption algorithm |
| A192CBC-HS384 |
AES_192_CBC_HMAC_SHA_384 authenticated encryption algorithm |
| A256CBC-HS512 |
AES_256_CBC_HMAC_SHA_512 authenticated encryption algorithm |
| A128GCM |
AES GCM using 128-bit key |
| A192GCM |
AES GCM using 192-bit key |
| A256GCM |
AES GCM using 256-bit key |
常见加密算法与分类
非对称加密
非对称加密算法加密数据和解密数据使用不同的密钥,分为公钥、私钥。数据经过公钥加密后,可以使用私钥校验或解密。
一般来说数据接受方会生成一对公钥、私钥,然后将公钥公开。数据发布方使用公开的公钥加密数据,接收方使用私钥 解密/校验 数据。
在传输过程中,如果是加解密算法,即时数据被截获,第三方没有私钥也无法完成数据解密;如果是签名算法,即时修改了数据,也无法修改签名,会被校验出数据异常。
非对称加密的缺点:
- 加密速度慢;
非对称加密的优点:
- 数据传输安全,可以检测出数据是否被篡改。
RSA
RSA 算法可以进行数据加密、解密。
RSA 算法安全强度与密钥位数有关,位数越长则越难破解,同时加密也更耗时。主流长度有 1024位、2048位、4096位,目前低于1024位的密钥已经能被暴力破解,推荐使用 2048位密钥长度。
RS256 是 RSA-SHA256 算法的简写。可以使用 OpenSSL 生成 RSA 公私钥。
# 1. 生成 2048 位的 RSA 密钥,默认PKCS#1格式 openssl genrsa -out rsa-private-key.pem 2048 # 1.1 将 PKCS#1 格式私钥转为 PKCS#8 openssl pkcs8 -topk8 -inform PEM -in rsa-private-key.pem -outform PEM -nocrypt -out rsa-private-key-pkcs8.pem # 1.2 PKCS#8 私钥转为 PKCS#1 格式 openssl rsa -pubin -in rsa-private-key-pkcs8.pem -RSAPublicKey_out -out rsa-private-key.pem # 2. 通过密钥生成公钥 openssl rsa -in rsa-private-key.pem -pubout -out rsa-public-key.pem
ECDSA
ECDSA 算法密钥可以使用 OpenSSL 生成。
# 1. 生成 ec 算法的私钥,使用 prime256v1 算法,密钥长度 256 位。(强度大于 2048 位的 RSA 密钥) openssl ecparam -genkey -name prime256v1 -out ecc-private-key.pem # 2. 通过密钥生成公钥 openssl ec -in ecc-private-key.pem -pubout -out ecc-public-key.pem
对称加密
对称加密算法在加密和解密过程使用同一个密钥。
对称加密的优点:
- 计算量小,加解密速度快;
对称加密的缺点:
- 密钥单一,需妥善保管;
AES
AES 是常见的对称加密算法,密钥长度一般为 128位、192位、256位。更多细节请参考:AES算法详解_三文鱼先生的博客-CSDN博客_aes算法
签名算法
个人理解签名算法即信息摘要算法,通过数据原文与密钥计算得到信息摘要(签名),信息摘要无法反推得到原文。因此信息摘要可用于验证原文是否被修改。
HMAC
HMAC 信息摘要算法分为 MD 和 SHA 两大类,常见算法如:MD5、SHA256、SHA384、SHA512。
注意 HS256 算法要求的密钥要有 256 位,在 Java 中单个字符占8位,因此密钥文本(如“secret-text-sample”字符串)至少要有32个字符。
RFC 7519 - JSON Web Token (JWT)
RFC 7515 - JSON Web Signature (JWS)
RFC 7516 - JSON Web Encryption (JWE)
深入了解Json Web Token之概念篇 - FreeBuf网络安全行业门户
How to generate a JSON Web Key (JWK) | Connect2id