代码

代码仓库:地址

代码分支: lesson7

博客:地址

简介

在上一篇文章中,我们使用SpringSecurity OAuth2 + SpringCloud Gateway搭建了一套符合微服务架构的授权系统,在Gateway网关实现统一身份鉴定、访问权限控制,同时将授权信息下发到下游业务服务中,下游业务服务只需要关注核心业务逻辑。上述架构依赖于auth授权服务器,每一次业务请求都需要使用access_token请求auth授权服务器来获取用户授权信息,如果access_token自带授权信息,那么网关只需要鉴别access_token有效信息,这将会降低系统对auth授权服务器的依赖,JWT(JSON Web Token)将是很好的选择。

JWT

我们这里不详细介绍JWT,有兴趣的同学可以查看阮一峰老师的文章:JWT入门教程。JWT定义了一种数据结构,它由三部分组成:

  • Header,头部,定义了签名算法,令牌类型
  • Payload,负载,是一个JSON对象,包含实际应用中使用的数据,例如用户名,用户角色,注意这部分内容是不加密的,因此不能包含保密信息
  • Signature,签名,用于验证JWT是否有效,防止信息内容篡改

JWT内容是不加密的,可以使用在线工具解码信息,查看内容。例如有一个JWT格式Token:

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzb3VyY2UiLCJibG9nIl0sImV4X3VzZXJuYW1lIjoiYWRtaW4iLCJ1c2VyX25hbWUiOiJhZG1pbiIsInNjb3BlIjpbImFsbCIsInVzZXIiXSwiZXhwIjoxNjU0NTk0MTc2LCJhdXRob3JpdGllcyI6WyJhZG1pbiJdLCJqdGkiOiI2NTdjZmU0Yi05ZDBlLTRhNTUtYjJjOS1iZWE3MTA2YWJkYWIiLCJjbGllbnRfaWQiOiJibG9nIn0.CGQTlvCCwGWIuJBy_qNeX2YBEYYTy6W1FPXOll75P1jdEyvi_TDiTLE4AO2Fa9vtgdWKrtywgGi4kFWZw8mcRFmhVfl9ehdoPcN5Hmdnz-ybJuLWh0i1k0xqg6MsZryTR1wAweEggZkHsIdCZfOw-yPZFTKuhAgVL4d-12Uthb4

在线解析后得到信息如下:

gateway整合oauth2 redis Gateway整合Oauth2 jwt_ci

项目改造优化

优化分析

在先前文章中,我们将授权信息保存在OAuth授权服务器中,客户端必须通过请求OAuth授权服务器来获取授权信息,如果使用JWT,并且在JWT中保存相关授权信息,那么可以直接解析JWT获取授权信息(需要验证JWT是有有效)。

在先前的项目中,我们使用SpringSecurity OAuth2默认配置来创建access_token,实际上SpringSecurity OAuth2提供了对JWT格式access_token支持,我们只需要配置access_token的生成方式,需要修改OAuth授权服务中的Token生成方式,以及Gateway网关服务中的Token解析方式进行修改即可

生成JWT格式Token

使用SpringSecurity OAuth2时没有配置TokenService对象,将会默认使用DefaultTokenServices组件来管理access_token, 使用UUID.randomUUID().toString()方式生成access_token和refresh_token,因此token中不包含任何信息。我们需要配置新的TokenService对象来生成JWT格式Token。

SpringSecurity OAuth2提供了以下组件来生成JWT格式Token:

  • JwtTokenStore实现了TokenStore接口,用来管理access_token和refresh_token
  • JwtAccessTokenConverter实现了TokenEnhancer, AccessTokenConverter接口,可以生成JWT格式token

JWT签名算法我们选用安全性更高的非对称加密算法:RSA(在代码auth/src/test/java/com/hzchendou/blog/demo/RSAKeyTest中提供生成RSA Key方法),配置TokenService:

@Bean
public TokenStore tokenStore() {
   return new JwtTokenStore(accessTokenConverter());
}
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
  JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
  converter.setKeyPair(keyPair()); //非对称秘钥,具体参见代码
  return converter;
}

@Bean
public AuthorizationServerTokenServices tokenService() {
   DefaultTokenServices service = new DefaultTokenServices();
   service.setClientDetailsService(authClientDetailService);
   service.setSupportRefreshToken(true);
   service.setTokenStore(tokenStore);
   //令牌增强
   TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
   List<TokenEnhancer> tokenEnhancers = new ArrayList<>();
   tokenEnhancers.add(tokenEnhancer);
   tokenEnhancers.add(accessTokenConverter);
   tokenEnhancerChain.setTokenEnhancers(tokenEnhancers);

   service.setTokenEnhancer(tokenEnhancerChain);
   service.setAccessTokenValiditySeconds(60 * 60 * 2); // 令牌默认有效期2小时
   service.setRefreshTokenValiditySeconds(60 * 60 * 24 * 3); // 刷新令牌默认有效期3天
   return service;
}

将TokenService配置到OAuth服务中:

endpoints.tokenServices(tokenService());//令牌管理服务

解析JWT格式Token

在网关中需要配置JWT格式解析器,因为使用RSA算法,因此在Gateway中需要配置RSA公钥来验证Token有效性,这里与SpringSecurity OAuth2推荐的配置方式有所不同,如果你使用SpringSecurity OAuth2的推荐方式,那么需要配置jwtAuthenticationConverter来解析JWT中的字段:

@Bean
public Converter<Jwt, ? extends Mono<? extends AbstractAuthenticationToken>> jwtAuthenticationConverter() {
  JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
  jwtGrantedAuthoritiesConverter.setAuthorityPrefix("");
  jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName("authorities");
  JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
  jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);
  return new ReactiveJwtAuthenticationConverterAdapter(jwtAuthenticationConverter);
}

下面还需要配置JWT解析验证器来验证JWT的有效性,有多种方式:

SpringSecurity OAuth2提供的方式:

除此之外还需要配置public key信息来验证JWT有效性,在配置文件中配置:

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          jwk-set-uri: http://localhost:8081/key/public-key /// 这里需要在oauth授权服务器中配置接口

@RestController
public class KeyController {
    @Autowired
    KeyPair keyPair;
    //获取公钥
    @GetMapping("/key/public-key")
    public Map<String, Object> getPublicKey() {
        RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
        RSAKey key = new RSAKey.Builder(publicKey).build();
        return new JWKSet(key).toJSONObject();
    }
}

直接配置PublicKey方式:

我们这里直接将public key 配置到gateway网关中:

jwt:
  rsa:
    publickey: MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCEfoWyfxqYz6j6tczCoELJfCwxpC+iHox7YEvz6slxNworp+CQAC86qt4Rx14lijoufiBMol0/mAABlG1lv3K1LOgQGcwueZDY5nk0uabOWv787moVbQHRTQoAwMIeSDPQ3SgSoEFyHM6Jj/We7XUpAyQEXKk9AabAvywEk2u9ewIDAQAB

然后手动创建JwtDecoder:

@Slf4j
@Configuration
public class TokenConfig {
    @Value("${jwt.rsa.publickey}")
    private String publicKey;
    public RSAPublicKey rsaPublicKey() {
        try {
            return (RSAPublicKey)RSAUtils.decodePublicKey(publicKey);
        } catch (Exception ex) {
            log.error("生成 KeyPair 失败", ex);
            System.exit(-1);
            return null;
        }
    }
    @Bean
    public NimbusReactiveJwtDecoder nimbusReactiveJwtDecoder() {
        return NimbusReactiveJwtDecoder.withPublicKey(rsaPublicKey())
                .signatureAlgorithm(SignatureAlgorithm.from("RS256")).build();
    }
}

还有一步需要配置,JWT Token解析后的类型是JwtAuthenticationToken,因此需要修改SecurityGlobalFilter中ReactiveSecurityContextHolder.getContext()方法返回的authentication类型(具体参见代码)

运行校验

分别运行auth、resource、gateway服务:

  • gateway - 8080
  • auth - 8081
  • resource - 8082

发起OAuth密码授权请求POST http://localhost:8080/blog-oauth/oauth/token:请求参数:

client_id:blog
client_secret:blog
grant_type:password
username:admin
password:admin

请求结果:

{
    "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzb3VyY2UiLCJibG9nIl0sImV4X3VzZXJuYW1lIjoiYWRtaW4iLCJ1c2VyX25hbWUiOiJhZG1pbiIsInNjb3BlIjpbImFsbCIsInVzZXIiXSwiZXhwIjoxNjU0NTk4MzY3LCJhdXRob3JpdGllcyI6WyJhZG1pbiJdLCJqdGkiOiIyMmVlOTU4My00Y2U5LTRmNzEtOGI4MS02YjViNzdmYWNlYWEiLCJjbGllbnRfaWQiOiJibG9nIn0.atWwzwpCK1ycjf3-EkPUYs4DMqO7rGPIMwMjHKS3FrTKRjMW5DHkQjtilG2EB8qGNBlwQJo0xAnQ_RNMzOjVojGxyb-TUPCubqODnmnYhuee0ho2TurDT5YzfO-Ypkv2SDqEm6Kw38m-oV_93NofGtKNJD1or2kwdoZe6kn4qgw",
    "token_type": "bearer",
    "refresh_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzb3VyY2UiLCJibG9nIl0sImV4X3VzZXJuYW1lIjoiYWRtaW4iLCJ1c2VyX25hbWUiOiJhZG1pbiIsInNjb3BlIjpbImFsbCIsInVzZXIiXSwiYXRpIjoiMjJlZTk1ODMtNGNlOS00ZjcxLThiODEtNmI1Yjc3ZmFjZWFhIiwiZXhwIjoxNjU0ODUwMzY3LCJhdXRob3JpdGllcyI6WyJhZG1pbiJdLCJqdGkiOiJjMzZhNDQ2Mi00ZmE3LTQ2OGUtODNiMS1iYzk4MDFjMzBjMWEiLCJjbGllbnRfaWQiOiJibG9nIn0.IyBeBQMjU-KYGIvvlQTrTkEtrPmTjLZIl1oFvyK0vytOlOFaE4Q5tMOLf1lt1UaBpmi2Tz4ElQSc6EMYX_OKmbyEHSidYxseUr8gE5MVM1raqOPCnR0Dyn7okQ0NvArOB9JuxLTXSa3NoSM3OxRQm2sUS55e6FKpifZ2q7xgGnY",
    "expires_in": 7199,
    "scope": "all user",
    "ex_username": "admin",
    "jti": "22ee9583-4ce9-4f71-8b81-6b5b77faceaa"
}

发起资源服务器请求:POST http://localhost:8080/blog-resource/admin/hello,请求头携带token参数:

Authorization:Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzb3VyY2UiLCJibG9nIl0sImV4X3VzZXJuYW1lIjoiYWRtaW4iLCJ1c2VyX25hbWUiOiJhZG1pbiIsInNjb3BlIjpbImFsbCIsInVzZXIiXSwiZXhwIjoxNjU0NTk2OTQyLCJhdXRob3JpdGllcyI6WyJhZG1pbiJdLCJqdGkiOiJkOWRhZjhmYS1jOTY4LTQ2YzQtODMyMi1kOTQxNGU3YWZhY2UiLCJjbGllbnRfaWQiOiJibG9nIn0.Jkz_Tlk1W7opXspihyDkp1VFcIXu3ebfVPkshjFcKpktPmqkUIA4D2aWF5A13fq5QGUIDQKf89rVeGHaFfer657J7kqaax2qNT6yuNgmQAu4C8VQkG01VLDsOa-m9xaZnqR_--Af-Z7FbwpZNOT2pBuyP4M3efnMmGhRQQjB4hQ

返回结果:

{
    "code": 200,
    "data": "Hello Admin"
}

结果符合预期,到此完成JWT + SpringSecurity OAuth2 + SpringCloud Gateway 统一权限访问控制功能

总结

  • JWT自带用户信息,只需要验证Token有效性
  • OAuth授权服务添加JWT TokenService返回JWT格式token
  • Gateway网关服务添加JWTAuthenticationConverter解析JWT信息,同时添加NimbusReactiveJwtDecoder对JWT有效性进行验证

参考文档