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)被称为 JWK(JSON Web Key)。
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。 |
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)