JWT&JOSE

引用: 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
  • 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
在这里插入图片描述
由此可见,headerpayload通过base64UrlDecode后,即可获取内容. 所以不适合将敏感信息通过JWT传递,仅适合用于token认证场景下使用。

弊端2: 微服务场景下秘钥泄露

通常情况下,我们使用JWT的流程如下图:
在这里插入图片描述

微服务场景下
在这里插入图片描述

微服务场景下,SECRET存放在各个微服务中,十分容易泄露,从而被不法分子所利用。



JOSE

在解决上述JWT存在的问题前,我们先了解下JOSE.
JOSE (Javascript Object Signing and Encryption) 定义了一系列标准,用来规范在网络传输中使用 JSON 的方式。
在这里插入图片描述

JOSE架构有三要素:

  1. JWA(JSON Web Algorithms):算法,它为每种算法定义了具体可能存在哪些参数,和参数的表示规则。
  2. JWK(JSON Web Key):秘钥,解决的是如何使用 JSON 来表示一个密钥这件事。
  3. 具体实现类型。 我们常说的JWT其实是一个统称,它的具体实现可以分为JWSJWE
    • 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组成在这里插入图片描述

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 规范还引入了两个新元素enczip
    • enc(content encryption algorithm):定义了内容加密算法,它应该是一个对称加密算法.
    • alg: 定义了Content Encryption Key (CEK) 内容加密秘钥加密算法。它应该是一个非对称加密算法.`
    • zip: 压缩算法(可选).
  • Encrypted Key(被加密的加密密钥)加密秘钥ContentEncryptionKey(CEK)用来加密密文,该字段为CEK的加密值。
  • Initialization Vector (初始化向量) :一些加密算法需要额外的数据(通常是随机的),防止秘钥重复。
  • Encrypted Data (Ciphertext) (加密的数据) :被加密的密文的base64url 编码值。
  • Authentication Tag (认证标签) :算法产生的附加数据,可用于验证密文内容不被篡改。

确保CiphertextAdditional Authenticated Data (AAD)的完整性。

JWE加解密过程

服务端定义了publicKeyprivateKey秘钥对

JWE生成过程
这五个部分的生成,也就是 JWE 的加密过程可以分为 7 个步骤:

  1. 预定义或随机生成Content Encryption Key (CEK)
  2. 根据 alg算法,对CEK进行加密,生成JWE Encrypted Key.

    加密过程 Encrypted Key = alg(publicKey,CEK)

  3. base64url-encoded(JWE Encrypted Key),这是JWE的第二个元素。
  4. 计算所选算法所需大小的 Initialization Vector (IV)。并对它进行base64url编码,这是JWE的第三个元素。
  5. 如果 Header 声明了 zip ,则压缩payload明文。
  6. base64url-encoded(JOSE header),这是JWE的第一个元素。
  7. 使用 CEKIV上一步中编码的 JOSE 标头的 ASCII 值,并将其用作 AAD。,通过Header enc声明的算法来加密内容,结果为Ciphertext Authentication Tag

    enc(CEK,IV,header,plainText)

  8. 分别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 KeyIV
10. 解密JWE Encrypted Key获取CEK

解密过程为:CEK = alg(privateKey ,EncryptedKey)

  1. 验证签名
  2. 通过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());
    }
将一个 JWK 转换为 X509Certificate 是不可能的,因为它们是不同的密钥格式。JWK 是 JSON Web Key 的缩写,它是一种用于表示密钥的 JSON 格式。而 X509Certificate 是一种常用的公钥证书格式,用于证明公钥的合法性和身份。 如果你需要将 JWK 转换为 X509Certificate,你需要先将 JWK 转换为一个 Java 的密钥对象,然后再使用 Java 的密钥工具将其转换为 X509Certificate,具体实现步骤如下: 1.使用 Nimbus JOSE+JWT 库将 JWK 转换为 Java 的密钥对象: ```java import com.nimbusds.jose.jwk.JWK; import com.nimbusds.jose.jwk.RSAKey; import java.security.interfaces.RSAPublicKey; JWK jwk = JWK.parse(jwkJsonString); RSAKey rsaJWK = (RSAKey) jwk; RSAPublicKey publicKey = rsaJWK.toRSAPublicKey(); ``` 2.使用 Java 的密钥工具将密钥对象转换为 X509Certificate: ```java import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.security.cert.CertificateException; import java.security.cert.CertificateEncodingException; import java.security.cert.Certificate; public static X509Certificate getCertificateFromPublicKey(RSAPublicKey publicKey) throws CertificateEncodingException, CertificateException { CertificateFactory cf = CertificateFactory.getInstance("X.509"); byte[] certBytes = publicKey.getEncoded(); Certificate cert = cf.generateCertificate(new ByteArrayInputStream(certBytes)); return (X509Certificate) cert; } ``` 请注意,这里的代码示例仅为示范代码,实际使用时需要根据具体情况进行调整。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值