美文网首页
使用jjwt解析基于Spring Security OAuth2

使用jjwt解析基于Spring Security OAuth2

作者: NealLemon | 来源:发表于2021-05-24 09:30 被阅读0次

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

大体的SAAS 门户平台如下

认证.png

具体的就不多说了,在这个架构下,我使用 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();
        }
    }
}
token.png

这里需要注意:必须要使用 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(),我也是无意间发现的,这个要从我们封装的接口说起,如图所示

base64URL.png

小结

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

相关文章

网友评论

      本文标题:使用jjwt解析基于Spring Security OAuth2

      本文链接:https://www.haomeiwen.com/subject/eeuwjltx.html