引用: jwt-jws-and-jwe-for-not-so-dummies-b63310d201a3
引用:https://onevcat.com/2018/12/jose-2/
引用:https://zhuanlan.zhihu.com/p/465992153
JWT
随着移动互联网的兴起,传统基于session/cookie的web网站认证方式转变为了基于OAuth2等开放授权协议的单点登录模式(SSO),相应的基于服务器session+浏览器cookie的Auth手段也发生了转变,Json Web Token出现成为了当前的热门的Token Auth机制。
JSON Web Token(JWT)是一个非常轻巧的规范。这个规范允许我们使用JWT在两个组织之间传递`安全可靠的信息。
示例
可以看出 JWT 以不同颜色区分,通过两个小数点隔开,分为了三部分:
- Header:JSON 对象,描述 JWT 的元数据。
- alg(algorithm) :签名的算法,默认是
HMAC+SHA256(HS256)
; - typ(token type) :令牌类型,
统一写为 JWT
。
- alg(algorithm) :签名的算法,默认是
- Payload:JSON 对象,存放实际需要传递的数据。payload通常会有一些保留字段如下:
“iss” (Issuer)
:JWT 的签发者名字,一般是公司名或者项目名“sub” (Subject)
:JWT 的主题“exp” (Expiration Time)
:过期时间,在这个时间之后应当视为无效“iat” (Issued At)
:发行时间,在这个时间之前应当视为无效
- Signature:对前面两部分内容使用秘钥
SECRET签名
,防止内容被篡改。
JWT弊端1: 信息泄露
jwtToken
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkQuVHJ1bXAiLCJyb2xlcyI6WyJleC1wcmVzaWRlbnQiLCJhY3RvciJdLCJhZ2UiOjc2LCJpYXQiOjE1MTYyMzkwMjJ9.WbpNE3f22yUkXLMw_DCOPhu-vx0YoCjPMRsKkDfFV5
生成公式:
base64UrlEncode(header).base64UrlEncode(payload).
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
SECRET
)
base64UrlDecode
由此可见,header
和payload
通过base64UrlDecode后,即可获取内容. 所以不适合将敏感信息通过JWT传递,仅适合用于token认证场景下使用。
弊端2: 微服务场景下秘钥泄露
通常情况下,我们使用JWT的流程如下图:
微服务场景下
微服务场景下,SECRET
存放在各个微服务中,十分容易泄露,从而被不法分子所利用。
JOSE
在解决上述JWT存在的问题前,我们先了解下JOSE.
JOSE (Javascript Object Signing and Encryption) 定义了一系列标准,用来规范在网络传输中使用 JSON 的方式。
JOSE架构有三要素:
- JWA(JSON Web Algorithms):
算法
,它为每种算法定义了具体可能存在哪些参数,和参数的表示规则。 - JWK(JSON Web Key):
秘钥
,解决的是如何使用 JSON 来表示一个密钥这件事。 - 具体实现类型。 我们常说的JWT其实是一个统称,它的具体实现可以分为JWS和JWE。
- JWS(JSON Web Signature) : ,它的核心就是签名,保证数据未被篡改,检查签名的过程就叫做验签。 前文的使用方式,就是JWS.
- JWE(JSON Web Encryption): JWS 的 Payload是Base64Url的
明文
,JWE的数据则是经过加密的,它可以使JWT更加安全。
JWA
JWA(JSON Web Algorithms),JOSE 体系中涉及到的所有加密算法就是它来定义的。
在 JWT Header 中,”alg” 是必须指定的值,它表示这个 JWT 的签名方式
。
上例中 JWT 使用的是 HS256 进行签名,也就是使用 SHA-256 作为摘要算法的 HMAC
。常见的选择还有 RS256,ES256 等等。总结一下:
- HSXXX 或者说 HMAC:
一种对称算法 (symmetric algorithm)
,也就是加密密钥和解密密钥是同一个。类似于我们创建 zip 文件时设定的密码,验证方需要知道和签名方同样的密钥,才能得到正确的验证结果。
- RSXXX:使用 RSA 进行签名。RSA 是一种基于极大整数做因数分解的
非对称算法 (asymmetric algorithm)
。 相比于对称算法的 HMAC 只有一对密钥,RSA 使用成对的公钥 (public key) 和私钥 (private key) 来进行签名和验证
。大多数 HTTPS 中验证证书和加密传输数据使用的是 RSA 算法。
- ESXXX:使用
椭圆曲线数字签名算法 (ECDSA)
进行签名。和 RSA 类似,它也是一种非对称算法
。不过它是基于椭圆曲线的。ECDSA 最著名的使用场景是比特币的数字签名。
- PSXXX: 和
RSXXX
类似类似使用 RSA 算法,但是使用 PSS 作为padding进行签名。作为对比,RSXXX 中使用的是 PKCS1-v1_5 的 padding。
JKW
不管签名验证还是加密解密
,都离不开密钥。JWK (JSON Web Key) 解决的是如何使用 JSON 来表示一个密钥这件事。
RSA
RSA 的公钥由模数 (modulus) 和指数 (exponent) 组成,一个典型的代表 RSA 公钥的 JWK 如下:
{
"alg": "RS256",
"n": "ryQICCl6NZ5gDKrnSztO3Hy8PEUcuyvg_ikC-VcIo2SFFSf18a3IMYldIugqqqZCs4_4uVW3sbdLs_6PfgdX7O9D22ZiFWHPYA2k2N744MNiCD1UE-tJyllUhSblK48bn-v1oZHCM0nYQ2NqUkvSj-hwUU3RiWl7x3D2s9wSdNt7XUtW05a_FXehsPSiJfKvHJJnGOX0BgTvkLnkAOTdOrUZ_wK69Dzu4IvrN4vs9Nes8vbwPa_ddZEzGR0cQMt0JBkhk9kU_qwqUseP1QRJ5I1jR4g8aYPL_ke9K35PxZWuDp3U0UPAZ3PjFAh-5T-fc7gzCs9dPzSHloruU-glFQ",
"use": "sig",
"kid": "b863b534069bfc0207197bcf831320d1cdc2cee2",
"e": "AQAB",
"kty": "RSA"
}
- alg:
- kty:
- e: 如果你接触过几个 RSA 密钥,可能会发现 “e” 的值基本都是
“AQAB”
。这并不是巧合,这是数字 65537 (0x 01 00 01) 的 Base64Url 表示。选择 AQAB 作为指数已经是业界标准,它同时兼顾了运算效率和安全性能。
ECDSA
{
"kty":"EC",
"alg":"ES256",
"use":"sig",
"kid":"3829b108279b26bcfcc8971e348d116",
"crv":"P-256",
"x":"EVs_o5-uQbTjL3chynL4wXgUg2R9q9UU8I5mEovUf84",
"y":"AJBnuQ4EiMnCqfMPWiZqB4QdbAd0E7oH50VpuZ1P087G"
}
决定一个 ECDSA 公钥的参数有三个:
- “crv”: 定义使用的密钥所使用的加密曲线,一般可能值为 “P-256”,”P-384” 和 “P-521”。
- ”x” 和 “y”:选取的椭圆曲线点的座标值
PEM
显然 JWK 是一种密钥的表现形式,它使用 JSON 的方式,遵守 JWA 的参数,来定义密钥。不过这种表现形式在日常里使用得并不是那么普遍,我们在平时看到得更多的也许是PEM (Privacy-Enhanced Mail)
秘钥。
RSA公钥的PEM
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAryQICCl6NZ5gDKrnSztO
3Hy8PEUcuyvg/ikC+VcIo2SFFSf18a3IMYldIugqqqZCs4/4uVW3sbdLs/6PfgdX
7O9D22ZiFWHPYA2k2N744MNiCD1UE+tJyllUhSblK48bn+v1oZHCM0nYQ2NqUkvS
j+hwUU3RiWl7x3D2s9wSdNt7XUtW05a/FXehsPSiJfKvHJJnGOX0BgTvkLnkAOTd
OrUZ/wK69Dzu4IvrN4vs9Nes8vbwPa/ddZEzGR0cQMt0JBkhk9kU/qwqUseP1QRJ
5I1jR4g8aYPL/ke9K35PxZWuDp3U0UPAZ3PjFAh+5T+fc7gzCs9dPzSHloruU+gl
FQIDAQAB
-----END PUBLIC KEY-----
ECDSA公钥的PEM
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEVs/o5+uQbTjL3chynL4wXgUg2R9
q9UU8I5mEovUf86QZ7kOBIjJwqnzD1omageEHWwHdBO6B+dFabmdT9POxg==
-----END PUBLIC KEY-----
JWK与PEM相互转换
JWS(略)
JWS对应前面提到的 JWT 的第三部分Signature
,所以我才会说我们日常所使用的JWT都是JWS。
JWS组成![在这里插入图片描述](https://img-blog.csdnimg.cn/a9fcf0bfc67943f4aa6afbe54eb9f1d8.png)
JWE
我们说过,经过Signature签名后的 JWT 就是指的JWS
,而 JWS 仅仅是对前两部分签名,保证无法篡改,但是其 Payload信息是暴露的。因此,使用JWS方式的Payload是不适合传递敏感数据的,JWT 的另一种实现 JWE 就是来解决这个问题的。
JWE组成
JWE有五个部分组成(四个小数点隔开).一个 JWE 示例如下:
eyJhbGciOiJSU0ExXzUiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0.
UGhIOguC7IuEvf*NPVaXsGMoLOmwvc1GyqlIKOK1nN94nHPoltGRhWhw7Zx0-kFm1NJn8LE9XShH59*
i8J0PH5ZZyNfGy2xGdULU7sHNF6Gp2vPLgNZ\_\_deLKxGHZ7PcHALUzoOegEI-8E66jX2E4zyJKxYxzZIItRzC5hlRirb6Y5Cl_p-ko3YvkkysZIFNPccxRU7qve1WYPxqbb2Yw8kZqa2rMWI5ng8Otv
zlV7elprCbuPhcCdZ6XDP0_F8rkXds2vE4X-ncOIM8hAYHHi29NX0mcKiRaD0-D-ljQTPcFPgwCp6X-nZZd9OHBv-B3oWh2TbqmScqXMR4gp_A.
AxY8DCtDaGlsbGljb3RoZQ.
KDlTtXchhZTGufMYmOYGS4HffxPSUrfmqCHXaI9wOGY.
9hH0vgRfYgPnAHOd8stkvw
JWE由五部分组成:
- Protected Header (受保护的头部) :类似于JWS的 Header ,标识加密算法和类型。区别是JWE 规范还引入了两个新元素
enc
和zip
- enc(content encryption algorithm):定义了内容加密算法,它应该是一个
对称加密算法.
- alg: 定义了
Content Encryption Key (CEK)
内容加密秘钥的加密算法
。它应该是一个非对称加密算法.` - zip: 压缩算法(可选).
- enc(content encryption algorithm):定义了内容加密算法,它应该是一个
- Encrypted Key(被加密的加密密钥) :
加密秘钥ContentEncryptionKey(CEK)
用来加密密文,该字段为CEK的加密值。 - Initialization Vector (初始化向量) :一些加密算法需要额外的数据(通常是随机的),防止秘钥重复。
- Encrypted Data (Ciphertext) (加密的数据) :被加密的密文的base64url 编码值。
- Authentication Tag (认证标签) :算法产生的附加数据,可用于验证密文内容不被篡改。
确保
Ciphertext
和Additional Authenticated Data (AAD)
的完整性。
JWE加解密过程
服务端定义了publicKey
和privateKey
秘钥对。
JWE生成过程
这五个部分的生成,也就是 JWE 的加密过程可以分为 7 个步骤:
- 预定义或随机生成
Content Encryption Key (CEK)
- 根据
alg
算法,对CEK进行加密,生成JWE Encrypted Key
.加密过程
Encrypted Key = alg(publicKey,CEK)
base64url-encoded(JWE Encrypted Key)
,这是JWE的第二个元素。- 计算所选算法所需大小的 Initialization Vector (IV)。并对它进行base64url编码,这是JWE的第三个元素。
- 如果 Header 声明了 zip ,则压缩payload明文。
base64url-encoded(JOSE header)
,这是JWE的第一个元素。- 使用
CEK
、IV
和上一步中编码的 JOSE 标头的 ASCII 值,并将其用作 AAD
。,通过Header enc
声明的算法来加密内容,结果为Ciphertext
和Authentication Tag
enc(CEK,IV,header,plainText)
- 分别base64url-encoded 上一步骤的两个结果,作为第4和5个元素。
BASE64URL-ENCODE(UTF8(JWE Protected Header)) ‘.’
BASE64URL-ENCODE(JWE Encrypted Key) ‘.’
BASE64URL-ENCODE(JWE Initialization Vector) ‘.’
BASE64URL-ENCODE(JWE Ciphertext) ‘.’
BASE64URL-ENCODE(JWE Authentication Tag)
JWE 相比 JWS 更加安全可靠,但是不够轻量,有点复杂。
JWE解密过程
9. BASE64URL-DECODE获取JWE Encrypted Key
和IV
10. 解密JWE Encrypted Key
获取CEK
解密过程为:
CEK = alg(privateKey ,EncryptedKey)
- 验证签名
- 通过enc对称解密cipherText,获取明文
缺陷修复
我们自然而然想到使用非对称加密来解决问题。
非对称加密中:
- 用公钥加密的过程叫
加密
- 用私钥解密的过程叫
解密
- 用私钥加密的消息称为
签名
,只有拥有私钥的用户可以生成签名. - 用公钥解密签名这一步称为
验证签名(验签)
,
缺陷1: 信息泄露
使用JWE 对敏感信息进行加密。
缺陷2: 秘钥泄露
使用私钥对 JWT 进行签名,公钥用于验证,也就是Token Server生产者持有私钥,Micro Server持有公钥。
安全性考虑
1.始终执行算法验证
签名算法的验证固定在后端,不以JWT 里的算法为标准。假设每次验证JWT,验证算法都靠读取 Header 里面的alg 属性来判断的话,攻击者只要签发一个"alg:none"
的JWT,就可以绕过验证了。
2.选择合适的算法
具体场景选择合适的算法,例如分布式场景下,建议选择 RS256
。
3.HMAC 算法的密钥安全
除了需要保证密钥不被泄露之外,密钥的强度也应该重视,防止遭到字典攻击。
4.避免敏感信息保存在JWT中
JWS 方式下的JWT的 Payload 信息是公开的,不能将敏感信息保存在这里,如有需要,请使用 JWE。
5.JWT 的有效时间尽量足够短
JWT 过期时间建议设置足够短,过期后重新使用refresh_token
刷新获取新的 token.
JOSE 实践
https://connect2id.com/products/nimbus-jose-jwt/examples/jwt-with-rsa-encryption
maven依赖
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>8.23</version>
</dependency>
JWT实现 - HMAC
@Test
public void jwtWithHmac() throws JOSEException, ParseException {
// 构建claims
JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
.subject("alice")
.issuer("https://c2id.com")
.expirationTime(new Date(new Date().getTime() + 60 * 1000))
.build();
// 构建jwt
SignedJWT signedJWT = new SignedJWT(new JWSHeader(JWSAlgorithm.HS256), claimsSet);
//使用秘钥secret创建HMAC签名器
JWSSigner signer = new MACSigner(secret);
//签名
signedJWT.sign(signer);
String token = signedJWT.serialize();
System.out.println("JWT token:\n" + token);
SignedJWT decodedJWT = SignedJWT.parse(token);
//创建HMAC验证器
JWSVerifier verifier = new MACVerifier(secret);
assertTrue("JWT token签名不合法!", decodedJWT.verify(verifier));
assertEquals("alice", signedJWT.getJWTClaimsSet().getSubject());
assertEquals("https://c2id.com", signedJWT.getJWTClaimsSet().getIssuer());
assertTrue(new Date().before(signedJWT.getJWTClaimsSet().getExpirationTime()));
}
JWS-RSA
@Test
public void jwsWithRsa() throws Exception {
//创建JWS头,设置签名算法和类型
JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.RS256).keyID(rsaKeyId).type(JOSEObjectType.JWT).build();
//将负载信息封装到Payload中
Payload payload = new Payload(new JSONObject(map));
//创建JWS对象
JWSObject jwsObject = new JWSObject(header, payload);
//使用秘钥secret创建Rsa签名器
RSAKey rsaJWK = new RSAKeyGenerator(2048)
.keyID(rsaKeyId)
.generate();
JWSSigner signer = new RSASSASigner(rsaJWK);
//签名
jwsObject.sign(signer);
String token = jwsObject.serialize();
System.out.println("RSA token:\n" + token);
//---------验证签名-------------//
JWSObject decodedJwsObject = JWSObject.parse(token);
//创建Rsa验证器,使用公钥验签
JWSVerifier verifier = new RSASSAVerifier(rsaJWK.toPublicJWK());
assertTrue("RSA token签名不合法!", decodedJwsObject.verify(verifier));
//从token中解析JWS对象
System.out.println("RSA payload:\n" + decodedJwsObject.getPayload().toString());
// ---- 查看 RSAkeys ------//
String publicKeyBase64 = new BASE64Encoder().encode(rsaJWK.toPublicKey().getEncoded());
String privateKeyBase64 = new BASE64Encoder().encode(rsaJWK.toPrivateKey().getEncoded());
System.out.println("RSA publicKey:\n-----BEGIN PUBLIC KEY-----\n" + publicKeyBase64+"\n-----END PUBLIC KEY-----");
System.out.println("RSA privateKey:\n-----BEGIN RSA PRIVATE KEY-----\n" + privateKeyBase64+"\n-----END RSA PRIVATE KEY-----");
// Convert to JWK format
JWK jwk = new RSAKey.Builder((RSAPublicKey)rsaJWK.toPublicKey())
.privateKey((RSAPrivateKey)rsaJWK.toPrivateKey())
.keyUse(KeyUse.SIGNATURE)
.keyID(UUID.randomUUID().toString())
.build();
Output the private and public RSA JWK parameters
System.out.println(jwk);
// Output the public RSA JWK parameters only
System.out.println(jwk.toPublicJWK());
}
JWE-RSA
@Test
public void jweWithRsa() throws JOSEException, ParseException, NoSuchAlgorithmException {
// Generate an RSA key pair
KeyPairGenerator rsaGen = KeyPairGenerator.getInstance("RSA");
rsaGen.initialize(2048);
KeyPair rsaKeyPair = rsaGen.generateKeyPair();
RSAPublicKey rsaPublicKey = (RSAPublicKey)rsaKeyPair.getPublic();
RSAPrivateKey rsaPrivateKey = (RSAPrivateKey)rsaKeyPair.getPrivate();
// 定义CEK
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
keyGen.init(256);
SecretKey cek = keyGen.generateKey();
/**
* 2. 构造header,并指明 alg 和 enc
* alg: 用来加密CEK的算法
* enc: 用来加密content的方法
*/
JWEAlgorithm alg = JWEAlgorithm.RSA_OAEP_256;
EncryptionMethod enc = EncryptionMethod.A128CBC_HS256;
JWEHeader jweHeader = new JWEHeader(alg, enc);
Payload payload = new Payload("Hello, world!");
// Create the encrypted JWE object
JWEObject jwe = new JWEObject(jweHeader, payload);
// Create an encrypter with the specified public RSA key
RSAEncrypter encrypter = new RSAEncrypter(rsaPublicKey);
// Do the actual encryption
jwe.encrypt(encrypter);
// Serialise to JWE compact form
String jweString = jwe.serialize();
System.out.println("JWE token:\n"+jweString);
//解密
JWEObject decodedJwe = JWEObject.parse(jweString);
RSADecrypter decrypter = new RSADecrypter(rsaPrivateKey);
decodedJwe.decrypt(decrypter);
System.out.println("JWE payload:\n"+decodedJwe.getPayload());
}