由于最近刚换了工作,好久没更新文章,这次是记录在调研设计的时候出现的问题,在网上搜了很多方法,都没有完美的解决掉jjwt解析通过RSA算法成圣的JWT 的完美解决方案,因此记录一下,希望有帮助到大家。
由于现在在做一些基础简单的架构工作,帮助部门改善和提高研发效率和产品质量,在架构设计一套比较符合部门发展的开发框架,在以JAVA生态圈选型的过程中,安全组件选择了 Spring Security ,也是为了以后做微服务以及中台中的安全做考虑,因为SC 已经集成了 Spring Security 功能。
大体的SAAS 门户平台如下

具体的就不多说了,在这个架构下,我使用 Spring Security OAuth2 生成JWT 进行统一验证,但是在Spring Cloud Gateway中 Spring Security 针对 Gateway的整合不太理想,他需要预先设置好 ReactiveAuthorizationManager的实现类,也就是说需要预先设置好过滤的RequestPath ,例如 如下的代码
@AllArgsConstructor
@EnableWebFluxSecurity
public class GatewaySecurityConfig {
private final PermissionAuthorizationManager permissionAuthorizationManager;
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
http.oauth2ResourceServer().jwt();
http
.authorizeExchange()
.pathMatchers("/token/**").permitAll() //设置放行路径
.anyExchange().access(permissionAuthorizationManager); //其他使用认证
http.csrf().disable();
return http.build();
}
}
如果不如上硬编码去过滤RequestPath ,只能在 PermissionAuthorizationManager
做全局的处理,比如如下代码注释中处理
@Slf4j
@Component
public class PermissionAuthorizationManager implements ReactiveAuthorizationManager<AuthorizationContext> {
@Override
public Mono<AuthorizationDecision> check(Mono<Authentication> mono, AuthorizationContext authorizationContext) {
ServerWebExchange exchange = authorizationContext.getExchange();
//请求资源
String requestPath = exchange.getRequest().getURI().getPath();
return mono.map(auth -> new AuthorizationDecision(checkAuthorities(exchange, auth, requestPath))).defaultIfEmpty(new AuthorizationDecision(false));
}
//权限校验
private boolean checkAuthorities(ServerWebExchange exchange, Authentication auth, String requestPath) {
Jwt principal = (Jwt) auth.getPrincipal();
/**
* 根据 principal 中的信息结合 exchange 中的Request信息做路由判断
* 或者连接中间件或数据库做判断
*/
log.info("访问的URL是:{}用户信息:{}",requestPath, principal.getClaims().get("user_name"));
return true ;
}
}
以上的代码以及方案都是网上现成的,总体来说 如果中小项目没什么问题,但是在做软件项目总集或者项目迭代很快或者扩展很快的项目上,问题就比较明显,需要把很多的权限逻辑都放在 #checkAuthorities
方法中,违背了软件开发原则,耦合性很高并且程序的性能也会受到影响。
因此我的设计思路是根据API去做权限的控制,把不同的权限设计成不同的Filter(Spring Cloud Gateway 的设计思想 ,如果不了解,可以去官方看文档),可以当做插件来使用,这样做到了解耦,并且针对不同项目,只需要开发插件即可,不需要依赖 #checkAuthorities
这个方法进行代码的修改。
那么如果我们抛弃了 Spring Cloud Gateway 上使用Spring Security ,我们就需要自己去实现JWT的解析,这里就是问题的关键,由于我们的JWT生成是 Spring Security Server ,因此我们还是要遵循Spring Security 的 方式来获取秘钥,解析token。
首先使用JDK 工具生成RSA秘钥
使用JDK工具的keytool生成JKS密钥库(Java Key Store),并将youlai.jks放到resources目录
keytool -genkey -alias neal -keyalg RSA -keypass 123456 -keystore neal.jks -storepass 123456
-genkey 生成密钥
-alias 别名
-keyalg 密钥算法
-keypass 密钥口令
-keystore 生成密钥库的存储路径和名称
-storepass 密钥库口令
使用代码生成秘钥对
public KeyPair keyPair() {
KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("neal.jks"), "123456".toCharArray());
KeyPair keyPair = keyStoreKeyFactory.getKeyPair("neal");
return keyPair;
}
对外暴露秘钥获取接口
@GetMapping("/getPublicKey")
public Map<String, Object> getPublicKey() {
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAKey key = new RSAKey.Builder(publicKey).build();
return new JWKSet(key).toJSONObject();
}
调用接口 获取到的JSON就是
{
"keys": [
{
"kty": "RSA",
"e": "AQAB",
"n": "udrVospaPWAZjQua_e6Fh3pep-ylcaFAWDrPmaxT2kjEbMIV38DaGN3Wkr1ZuJzqATzB483b_Ia6z9MZ4zUS0FMF2a5G7q2nEv1t77SgneXiQG6ncYEYBMLHStfLXUi18L3okRU00yA1LiXpBvTijcvOmxLz_ECojWg05J9hOh3OhTdmjCzsVlZi1dF9Ss7R5JEtvIDW_Ll7N5yE9kTFS4kg9SVtxzj5ThDk4TAR8TC7Dj_vQP_gNE6hPohkLbanYXuRHojEeUdUrjZc7a7aKyOTkJwngR5MJ1hi5SO1WgwOjWCscWErBmRY7uinThsxmf1R-xDDU6YG2u7G70Q8hw"
}
]
}
同时调用Spring Security 生成 JWT的接口 /oauth/token
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiZ2F0ZXdheSJdLCJ1c2VyX25hbWUiOiJuZWFsIiwic2NvcGUiOlsicmVhZCIsIndyaXRlIl0sImV4cCI6MTYyMTU4MzE3OCwiYXV0aG9yaXRpZXMiOlsiUk9MRV9BRE1JTiJdLCJqdGkiOiJmYzlmZjg5Zi1mM2I4LTQxNWYtYjVmNy0xMTZhYjIxZmVlOGQiLCJjbGllbnRfaWQiOiJnYXRld2F5In0.Bvqa1IYiaa9x37X1D2az0uN0ykZO1rXgvT1sZXtbHq5jA83-n5wOSqO0Uup4bGObu1VkEZJrP-uyMpKvNkcaNc8XlpCu7UCOQv1MSnKwXaCHklUTWL2y74aHR65UqyU2tG0wd4LYcJgvyB9GTO2ZTEI9r91aBqkjH1yIS7t5727pjJdOWnKcDPIwpVTS2Bm6hahFD-gkgAgWOwRHhqzFctcu7TZ23pm9NyE82_-IDO1Ey7AsARw3PyEjNrEYpt4qOLgP-tV9qS_0kLt-tU1EZXZJ1RmSQMW_DBDi78JcfyON_OWNVxwc3FjjqzJ-rlfTPBAZ916bGiROriNUc4HQaQ",
"token_type": "bearer",
"expires_in": 3599,
"scope": "read write",
"jti": "fc9ff89f-f3b8-415f-b5f7-116ab21fee8d"
}
具体参数我就不详细说了 之后如果平台设计并且落地后 会出个系列介绍。
拿到了access_token 以及 公钥 那么如何进行解析呢。
先给出代码
public class TokenTest {
public static void main(String[] args) {
String publicKeyStr = "公钥JSON里的 n "
String exponentStr = "AQAB"; // 公钥JSON里的 e
byte[] modulusBytes = Base64.getUrlDecoder().decode(publicKeyStr);
byte[] exponentBytes = Base64.getUrlDecoder().decode(exponentStr);
BigInteger modulusInt = new BigInteger(1, modulusBytes);
BigInteger exponentInt = new BigInteger(1, exponentBytes);
try {
RSAPublicKeySpec pubKeySpecification = new RSAPublicKeySpec(modulusInt, exponentInt);
KeyFactory keyFac = KeyFactory.getInstance("RSA");
RSAPublicKey pubKey = (RSAPublicKey)keyFac.generatePublic(pubKeySpecification);
JwtParser jwtParser = Jwts.parserBuilder().setSigningKey(pubKey).build();
String token = "access_token 字符串";
Claims claims = jwtParser.parseClaimsJws(token).getBody();
System.out.println(claims);
} catch (Exception e) {
e.printStackTrace();
}
}
}

这里需要注意:必须要使用 Base64.getUrlDecoder().decode()
方法,使用其他方式decode在JJWT验证的时候都会报错,主要报错的异常如下
Exception in thread "main" java.lang.IllegalArgumentException: Illegal base64 character 5f
Unable to verify RSA signature using configured PublicKey. Signature length not correct: got 512 but was expecting 256
....
话又说回来,为什么要使用 Base64.getUrlDecoder().decode()
,我也是无意间发现的,这个要从我们封装的接口说起,如图所示

小结
很简单的JJWT解析 JWT ,困扰了我一天,原因就是我们太相信网上的解决代码,之前总觉得是 生成秘钥的错误或者是 秘钥对和token字符串有问题,当回头咱们从新在看一遍之后,就会发现解决方法其实就在你写的代码中, 记录的有些乱,希望思路和方案给大家有所帮助。
网友评论