代码
代码仓库:地址
代码分支: 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
在线解析后得到信息如下:
项目改造优化
优化分析
在先前文章中,我们将授权信息保存在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有效性进行验证
参考文档