当前位置:   article > 正文

16-Spring Security中的JOSE类库_spring-security-oauth2-jose

spring-security-oauth2-jose

前面对JOSE规范进行了科普,今天我们来实践一波。Java庞大的生态圈自然也少不了对JOSE的支持。

本文DEMO:https://gitee.com/felord/spring-security-oauth2-tutorial jose 分支。

Nimbus-JOSE-JWT

很多教程会使用jjwt作为JWT的集成类库,它很精湛,对于JWT应该是够用了。但是如果要结合OAuth2的话,它远远不够。这里我推荐使用connect2id开源的nimbus-jose-jwt,功能齐全,API友好。更重要的是最新的Spring Security 5.xJOSE相关的都是用了该类库。

集成JOSE类库

这里我直接使用Spring Security二次封装的spring-security-oauth2-jose:

    <dependency>
      <groupId>org.springframework.security</groupId>
      <artifactId>spring-security-oauth2-jose</artifactId>
    </dependency>
  • 1
  • 2
  • 3
  • 4

如果你集成了Spring Security OAuth2 Client类库将自动集成该JOSE依赖库。

生成公私钥文件

针对不同的算法可能有不同的生成方法,这里我使用keytool生成2048长度的RSA密钥证书,命令如下 :

keytool -genkey -alias jose  -keyalg RSA -storetype PKCS12 -keysize 2048 -validity 365 -keystore d:/keystores/jose.jks -storepass felord.cn  -dname "CN=(felord), OU=(felord), O=(felord), L=(zz), ST=(hn), C=(cn)"
  • 1

该命令会生成一个jose.jks文件,alias值为jose,密码为felord.cn。它包含一对RSA公私钥。

我们还可以通过下面的命令提取出公钥文件pub.cer

 keytool -export -alias jose -keystore d:/keystores/jose.jks  -file pub.cer
  • 1

传统方法中,这个公钥文件将提供给消费方进行验签操作。

JWK生成

从现在起传统方式可以放弃了。

加载秘钥

先要通过java.security.KeyStore加载秘钥,

        KeyStore jks = KeyStore.getInstance("jks");
        // 对应keytool命令中的 alias
        String alias = "jose";
        // 对应keytool命令中的 storepass
        String storePass = "felord.cn";
        char[] pin = storePass.toCharArray();
        // 借用Spring 读取资源的方法获取密钥文件流
        jks.load(new ClassPathResource("jose.jks").getInputStream(), pin);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
生成JWK

nimbusds库提供了com.nimbusds.jose.jwk.RSAKey来封装RSA算法的JWK,加载方法很简单。

        RSAKey rsaJwks = RSAKey.load(jks, alias, pin);
        System.out.println("privateJWK = " + rsaJwks.toJSONString());
        RSAKey publicJWK = rsaJwks.toPublicJWK();
        System.out.println("publicJWK = " + publicJWK.toJSONString());
  • 1
  • 2
  • 3
  • 4

从公钥文件pub.cer中加载JWK也非常简单。

        CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
        ClassPathResource resource = new ClassPathResource("pub.cer");
        InputStream inputStream = resource.getInputStream();
        X509Certificate certificate = (X509Certificate) certificateFactory.generateCertificate(inputStream);

        RSAKey publicKey = RSAKey.parse(certificate);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

这里我分别打印了RSA公私钥的JWK,我们来看看结构。

RSA公钥的JWK:

{
  "kty": "RSA",
  "x5t#S256": "SxqqdWYxT7BZrdH-uVpgAHfX2q34qPyxx4onX6mv-qI",
  "e": "AQAB",
  "kid": "jose",
  "x5c": [
    "MIIDazCCAlOgAwIBAgIEVt9AMjANBgkqhkiG9w0BAQsFADBmMQ0wCwYDVQQGEwQoY24pMQ0wCwYDVQQIEwQoaG4pMQ0wCwYDVQQHEwQoenopMREwDwYDVQQKEwgoZmVsb3JkKTERMA8GA1UECxMIKGZlbG9yZCkxETAPBgNVBAMTCChmZWxvcmQpMB4XDTIyMDMwMTA3MzAyNloXDTIzMDMwMTA3MzAyNlowZjENMAsGA1UEBhMEKGNuKTENMAsGA1UECBMEKGhuKTENMAsGA1UEBxMEKHp6KTERMA8GA1UEChMIKGZlbG9yZCkxETAPBgNVBAsTCChmZWxvcmQpMREwDwYDVQQDEwgoZmVsb3JkKTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAI+qB4H4/ORguG22+htrp8bewvFF/Ftzy5GBXq53HxoTahMwByYpFAaxyt+gyS1tqO4q9JSo/ZBN6pIifpH7lzXiKtbm+g5BO+QFcTEMrGMXbGqtGhzkJo2+GY06fkW0yGvDa0VNRS4CVwXHnxLUcAoUOUOL+hUZnVWW+VTkCt1wjQOv5jHx1V7Y/C9DE+g3kM4R0y+ZnSoG24eXg9AG9Im2sXshfNAfOzgRVG609ykJnh9G3lacB+8XGJhMjk7QVr6RoDUFwAOCHzQ0Lm5pqRy/RZFwPK7adw6pqmApV9eM4H279n0IMkAe9OHcrQ8JrG9MA6S30D4SZahbso6PFusCAwEAAaMhMB8wHQYDVR0OBBYEFAsX0KhMretoF88OMXjznl2sbN4XMA0GCSqGSIb3DQEBCwUAA4IBAQA4Ad1K9lyfmeQ8qwTFyRfQn5Q0qCG28+NWDWFWnT6Qh0LAloVVstogbOYq8WKi8Em3KxAg7sS+XEnKBx7z9SOeq3/+00L3hpjOhhbc+lyY5Gu7jH4xacahzIsQG+PGDlynyfreTmQQa/61fCgXliz7NOAteoEzHxm8dzHARw+97zIcg6r8JCA9RCzZCXR8AGAULgxZ65klt0lSqxnUdd+qKtphHw656XucxRMdH1g/CMUnXLlCrW9mAcyITOkBOb942zVZX9iH7KjkTkXLE/TgTYjucl5iVd6ysdfbrhsUXiKwSHejXRjZRg3vEPmObaEPoYkpr+rCzwI+3oguUibX"
  ],
  "n": "j6oHgfj85GC4bbb6G2unxt7C8UX8W3PLkYFerncfGhNqEzAHJikUBrHK36DJLW2o7ir0lKj9kE3qkiJ-kfuXNeIq1ub6DkE75AVxMQysYxdsaq0aHOQmjb4ZjTp-RbTIa8NrRU1FLgJXBcefEtRwChQ5Q4v6FRmdVZb5VOQK3XCNA6_mMfHVXtj8L0MT6DeQzhHTL5mdKgbbh5eD0Ab0ibaxeyF80B87OBFUbrT3KQmeH0beVpwH7xcYmEyOTtBWvpGgNQXAA4IfNDQubmmpHL9FkXA8rtp3DqmqYClX14zgfbv2fQgyQB704dytDwmsb0wDpLfQPhJlqFuyjo8W6w"
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

私钥非常敏感,应禁止对外展示,因此这里我仅仅展示RSA私钥JWK结构:

{
  "p": "15cFgQp_8Sf3PZaFvVNbkj",
  "kty": "RSA",
  "x5t#S256": "SxqqdWYxT7BZrdH-uVpgAHfX2q34qPyxx4onX6mv-qI",
  "q": "qpeveODWsPrODDSIhKgy",
  "d": "fOyBMsfsQDrKpLzjp0xpzEiQg3U0B",
  "e": "AQAB",
  "kid": "jose",
  "x5c": [
    "MIIDazCCAlOgAwIBAgIEVt9AMjANBgkqhkiG9w0BAQsFADBmMQ0wCwYDVQQGEwQo"
  ],
  "qi": "kPG-qPAl472a0BIqGcCJq-VTxe",
  "dq": "ZguUkKdWZcmxpcVrAIeo2Bwf6G6bTm1Ock",
  "n": "j6oHgfj85GC4bbb6G2unxt7C8UX8W3PLkYFerncfGhNqEzA"
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

我相信有人会对上面JSONKey的感兴趣,在JOSE中已经对JWK公共部分的参数进行了解释,根据RFC7518提供的解读,我简单总结一下RSA JWK相关的特定参数:
在这里插入图片描述

再次强调任何算法的私钥都不应该公开访问。

JWK Set

JWK Set实际上就是一个JWK的集合。通过com.nimbusds.jose.jwk.JWKSet的构造方法初始化就可以了。

RSAKey rsaJwks = ...        
JWKSet jwkSet = new JWKSet(Collections.singletonList(rsaJwks));
  • 1
  • 2
JWKSource

nimbusds库的设计中,JWK并不能直接使用,需要提供一个可以检索匹配的的源,它被抽象为com.nimbusds.jose.jwk.source.JWKSource,它提供三种实现:

  • ImmutableJWKSet 不可变的JWKSet
  • ImmutableSecret 由不可变的SecretKey组成的JWKSet
  • JWKSecurityContextJWKSetJWKSecurityContext加载JWKSet
  • RemoteJWKSetURL请求中加载JWKSet

JWK的使用

JWK只是密钥的另一种形态,其作用依然是签名/验签、加密/解密,只不过这里是给JWSJWE提供该服务。 JWK借助于NimbusJwtEncoder就可以生成一个JWT

        JWKSource<SecurityContext> jwkSource = jwkSource() 
        JwtEncoder jwtEncoder = new NimbusJwtEncoder(jwkSource);
        JwsHeader jwsHeader = JwsHeader.with(SignatureAlgorithm.RS256)
                .jwkSetUrl("https://felord.cn/oauth2/jwks")
                .type("JWT")
                .build();

        Instant issuedAt = Clock.system(ZoneId.of("Asia/Shanghai")).instant();

        long exp = 604800L;
        Instant expiresAt = issuedAt.plusSeconds(exp);
        Instant notBefore = issuedAt.minusSeconds(60);

        JwtClaimsSet jwtClaimsSet = JwtClaimsSet.builder()
                .issuer("https://felord.cn")
                .subject("felord")
                .audience(Arrays.asList("https://client1.felord.cn",
                                        "https://client2.felord.cn"))
                .expiresAt(expiresAt)
                .issuedAt(issuedAt)
                .notBefore(notBefore)
                .id(UUID.randomUUID().toString())
                .claim("scope", Arrays.asList("message.read","message.write"))
                .build();

        JwtEncoderParameters parameters = JwtEncoderParameters
            .from(jwsHeader,jwtClaimsSet);
        Jwt jwt = jwtEncoder.encode(parameters);

        String token = jwt.getTokenValue();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

生成的JWT如下图:
在这里插入图片描述
很多同学不太清楚JWTClaims参数代表的意思,你可以参考下图:
在这里插入图片描述

JwtDecoder

有编码就有解码,解码自然使用了公钥JWK。借助于NimbusJwtDecoder我们可以将JWT字符串转换为Jwt对象。先构建一个JwkSetUri

    /**
     *  jwkSetUri端点,可以开放给特定的资源服务器
     *  
     * @return pub jwk
     */
    @SneakyThrows
    @GetMapping(value = "/oauth2/jwks")
    public Map<String, Object> jwks() {
        // 这里不重复RSAKey如何加载了
        // com.nimbusds.jose.jwk.RSAKey rsaKey = ... ;
        JWKSet jwkSet = new JWKSet(Collections.singletonList(rsaKey));
        // 这里只会输出公钥JWK
        return JSONObjectUtils.parse(jwkSet.toString());
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

jwkSetUriMediaType可以为application/jwk-set+json或者application/json

如果设置了校验规则需要全部通过校验才能被解码

    /**
     * 解析JWT
     */
    @SneakyThrows
    @Test
    public void jwtDecode() {
       final String token = "eyJ4NXQjUzI1NiI6IlN4cXFkV1l4VDdCWnJkSC11VnBnQUhmWDJxMzRxUHl4eDRvblg2bXYtcUkiLCJqa3UiOiJodHRwczpcL1wvZmVsb3JkLmNuXC9vYXV0aDJcL2p3a3MiLCJraWQiOiJqb3NlIiwidHlwIjoiSldUIiwiYWxnIjoiUlMyNTYifQ." +
                "eyJzdWIiOiJmZWxvcmQiLCJhdWQiOlsiaHR0cHM6XC9cL2NsaWVudDEuZmVsb3JkLmNuIiwiaHR0cHM6XC9cL2NsaWVudDIuZmVsb3JkLmNuIl0sIm5iZiI6MTY0NjIzNjY2Miwic2NvcGUiOlsibWVzc2FnZS5yZWFkIiwibWVzc2FnZS53cml0ZSJdLCJpc3MiOiJodHRwczpcL1wvZmVsb3JkLmNuIiwiZXhwIjoxNjQ2ODQxNTIyLCJpYXQiOjE2NDYyMzY3MjIsImp0aSI6IjQ3OGNmZmRmLTllNWYtNDlhNy1iNjlkLWI3YzFhNzY1YTNiOCJ9." +
                "BEcV65GcRqwaaaRI1TUI2s5b7K6ewyV5-7g_OTWCBuS-WzdJX4v5kS5YkK-4ABwaQWZJgNsV-zOxWvXBICSqHocs-oKd40Iiqz6DWFY8RrfqN-HwphELbPLyfrIWcJ7iVr3t-vF3NWcLZaPuv0PGEn4n4mkdQXpu59FDxUgX-XR_i-kSZwgiw_NgLd7z0UpLlD3Cm3kxnwAFAPf_V1eQWjKhZvXYto4ws-j0lZSf1LGDDRu8d5WS4hPRt6h4-x9-ZPZIoxHifhrPfVG3qQUZ0MlA1mKqfcrVUexgFqN8bcTP4krkwDbodsYVqQPHKFMWaIPHcLvHYp5_hkuzxCBT7A";

        // JwkController   需要远程端点支持。
        NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder
                .withJwkSetUri("http://localhost:8080/oauth2/jwks")
                .build();
        Jwt jwt = jwtDecoder.decode(token);
        Assertions.assertEquals("felord",jwt.getSubject());
        Assertions.assertEquals("https://felord.cn",jwt.getIssuer().toString());
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

到这里借助于JWKJWT的生成和解析就完成了。

你可以构建一个JwkSetUri进行远程加载测试。如果你在学习中遇到问题,可以留言,也可以联系我

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家自动化/article/detail/84435
推荐阅读
相关标签
  

闽ICP备14008679号