之前在SpringSecurity实战干货系列中,使用了Spring官方提供的spring-security-jwt作为JWT实现。目前该工具包已经不再维护,与最新的SpringSecurityOAuth2Client和SpringAuthorizationServer也不是特别兼容。于是花了两天时间结合这两个新的依赖重新实现了JWT。Nimbus库在最新的SpringSecurity中默认使用NimbusJOSE库nimbus-jose-jwt。这个库是目前JOSE中最常用的类库之一,大部分的改造工作都是围绕这个库进行的。改造过程分享过程和SpringSecurity的实际干货大致相同。加载证书证书仍然使用keytool生成2048长度的RSA密钥。之前使用比较“暴力”的方法,直接读取KeyStore,然后使用公钥和私钥。这次将KeyStore加载的证书转换成JOSE规范中的JWK(JsonWebKey)。JWTJWT在SpringSecurity中被定义为org.springframework.security.oauth2.jwt.Jwt对象,JWT的运行可以抽象为两个方面。生成JWT从生成JWT开始。目前的SpringSecurity本身并没有提供这个能力。只有孵化的SpringAuthorizationServer提供了生成JWT的抽象接口JwtEncoder:@FunctionalInterfacepublicinterfaceJwtEncoder{Jwtencode(JoseHeaderheaders,JwtClaimsSetclaims)throwsJwtEncodingException;}JWTHeader和Claims也相应的抽象为JoseHeader和JwtClaimsSet。所以我用Nimbus实现了JwtEncoder,其实就是承载了SpringAuthorizationServer的实现。当然不是照搬原版,只是保证门面一致,以便我们在项目成熟后无缝兼容。解析JWT既然有JwtEncoder,就必然有JwtDecoder。这是在SpringSecurityOAuth2Client中实现的,也稍作修改。另外,这个解码器除了负责将JWT字符串解析成JWT对象外,还承担了校验的功能。这里有一个委托验证器DelegatingOAuth2TokenValidator,我们可以灵活自定义实现多种JWT验证策略。我们都知道JWT中的token通常是成对出现的。之前只是简单的用一个类来封装accessToken和refreshToken的字符串形式。这次我使用了springsecurityoauth2core提供的OAuth2AccessTokenResponse:publicfinalclassOAuth2AccessTokenResponse{privateOAuth2AccessTokenaccessToken;privateOAuth2RefreshTokenrefreshToken;privateMapadditionalParameters;}这个类表达的内容更丰富、更灵活。对应的json:{"accessToken":{"tokenValue":"eyJraWQiOiJmZWxvcmRjbiIsInR5cCI6IkpXVCIsImFsZyI6IlJTMjU2In0.eyJzdWIiOiJhbGwiLCJhdWQiOiJyb290IiwiaXNzIjoiaHR0cHM6XC9cL2ZlbG9yZC5jbiIsInNjb3BlcyI6WyJST0xFX0FETUlOIiwiUk9MRV9BUFAiXSwiZXhwIjoxNjE2ODM4NTg4LCJpYXQiOjE2MTY4MzQ5ODgsImp0aSI6IjBiYTUwZjFhLTI0N2YtNDJlYi05NzZiLTkyZWM5NDg2YjA2MCJ9.dwUK4ZgqhalKWu5AA8ZqaHjD2WPerhiF8lmybZGAorbncWdfVk7iAKUdRZunUekZmab_FsVpwprWIQpqSLtp6tz28sI71gO2StEeye5Vv4JRZKys68q2LGOAqMVJnBisEl211b5ASHSlP1qleU_TDxO_rgems76ZFD-kc1KmyelsoiBhmT3aD2_A_3fUmH7mV0jnC0rHauzOpS0AWnuPJaXbGPqrWotkQ_oqly47jipfNsPl_PUY1urng1wSx4QyblS8UgK-n5wJABhSN550WlwNLuC10ZckbhE5gazM0mD86mA_Xepe7LY5rjGNvO-Cz9k44TaURnTdSBdyy_EOiQ","issuedAt":{"epochSecond":1616834988,"nano":891000000},"expiresAt":{"epochSecond":1616838588,"nano":891000000},"tokenType":{"value":"Bearer"},"scopes":["ROLE_ADMIN","ROLE_APP"]},"refreshToken":{"tokenValue":"eyJraWQiOiJmZWxvcmRjbiIsInR5cCI6IkpXVCIsImFsZyI6IlJTMjU2In0.eyJzdWIiOiJhbGwiLCJhdWQiOiJyb290IiwiaXNzIjoiaHR0cHM6XC9cL2ZlbG9yZC5jbiIsInNjb3BlcyI6WyJST0xFX0FETUlOIiwiUk9MRV9BUFAiXSwiaWF0IjoxNjE2ODM0OTg4LCJqdGkiOiI3N2RhODk3NC0xMjM0LTQ5NzctOWU1MS1hOGY2NTdjMzA2NjAifQ.O9YYxkevkrTke7GbK2R5LGphnJ9vd07yFSwPs2gEZ94ObPkIs1wJ5gvlNOIlni_BYMNO-nMB8TiX0w-RQSwo-sbVLqeUHqv6NEXXmPJiWVmXTFVJf2b6lqW5Re7clXGvkFMw14ptAF6cpThDEE5XF4eCI8CDKKPWqNxY-8NvokwIY3NMXB1ofuHHRqjMyVUwNjOv6eaTJFTwebPy6Saem9kvaL_X1v9Drok6azbg5DSP1zKnbVazTaOs4aBZd5Firib3r_BGXdaJWAgJKfpP61__muVdujgkppMVU8fC9pqfnb6IqEaAOIZ69lrezA1K0QFinOhgcC2YZFxFoLL-IQ","issuedAt":{"epochSecond":1616834988,"nano":891000000},"expiresAt":null},"additionalParameters":{}}总结一下,其他的东西没有太大变化,尽量保证原汁原味,并且可以兼容Spring以后的风格安全性。在代码迭代的过程中,我们如何做到兼容和灵活同一时间?关键在于是否制定了统一的入口抽象和出口抽象。如果你能做到这一点,你的代码质量显然会提高很多。