赞
踩
前面对JOSE规范进行了科普,今天我们来实践一波。Java庞大的生态圈自然也少不了对JOSE的支持。
本文DEMO:https://gitee.com/felord/spring-security-oauth2-tutorial jose 分支。
很多教程会使用jjwt
作为JWT的集成类库,它很精湛,对于JWT应该是够用了。但是如果要结合OAuth2的话,它远远不够。这里我推荐使用connect2id开源的nimbus-jose-jwt
,功能齐全,API友好。更重要的是最新的Spring Security 5.x和JOSE相关的都是用了该类库。
这里我直接使用Spring Security二次封装的spring-security-oauth2-jose
:
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-jose</artifactId>
</dependency>
如果你集成了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)"
该命令会生成一个jose.jks
文件,alias
值为jose
,密码为felord.cn
。它包含一对RSA公私钥。
我们还可以通过下面的命令提取出公钥文件pub.cer
:
keytool -export -alias jose -keystore d:/keystores/jose.jks -file pub.cer
传统方法中,这个公钥文件将提供给消费方进行验签操作。
从现在起传统方式可以放弃了。
先要通过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);
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());
从公钥文件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);
这里我分别打印了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"
}
私钥非常敏感,应禁止对外展示,因此这里我仅仅展示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"
}
我相信有人会对上面JSON中Key的感兴趣,在JOSE中已经对JWK公共部分的参数进行了解释,根据RFC7518提供的解读,我简单总结一下RSA JWK相关的特定参数:
再次强调任何算法的私钥都不应该公开访问。
JWK Set实际上就是一个JWK的集合。通过com.nimbusds.jose.jwk.JWKSet
的构造方法初始化就可以了。
RSAKey rsaJwks = ...
JWKSet jwkSet = new JWKSet(Collections.singletonList(rsaJwks));
在nimbusds库的设计中,JWK并不能直接使用,需要提供一个可以检索匹配的的源,它被抽象为com.nimbusds.jose.jwk.source.JWKSource
,它提供三种实现:
ImmutableJWKSet
不可变的JWKSet
。ImmutableSecret
由不可变的SecretKey
组成的JWKSet
。JWKSecurityContextJWKSet
从JWKSecurityContext
加载JWKSet
。RemoteJWKSet
从URL请求中加载JWKSet
。JWK只是密钥的另一种形态,其作用依然是签名/验签、加密/解密,只不过这里是给JWS、JWE提供该服务。 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();
生成的JWT如下图:
很多同学不太清楚JWT中Claims
参数代表的意思,你可以参考下图:
有编码就有解码,解码自然使用了公钥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());
}
jwkSetUri
的MediaType
可以为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()); }
到这里借助于JWK对JWT的生成和解析就完成了。
你可以构建一个
JwkSetUri
进行远程加载测试。如果你在学习中遇到问题,可以留言,也可以联系我。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。