前期内容导读:
- 开源加解密RSA/AES/SHA1/PGP/SM2/SM3/SM4介绍
- 开源AES/SM4/3DES对称加密算法介绍及其实现
- 开源AES/SM4/3DES对称加密算法的验证实现
- 开源非对称加密算法RSA/SM2实现及其应用
- 非对称加密算法RSA实现
- 开源非对称加密算法SM2实现
- Java开源接口微服务代码框架
- Json在开源SpringBoot/SpringCloud微服务中的应用
- 加解密在开源SpringBoot/SpringCloud微服务框架的最佳实践
- 链路追踪在开源SpringBoot/SpringCloud微服务框架的实践
+------------+
| bq-log |
| |
+------------+
Based on SpringBoot
|
|
v
+------------+ +------------+ +------------+ +-------------------+
|bq-encryptor| +-----> | bq-base | +-----> |bq-boot-root| +-----> | bq-service-gateway|
| | | | | | | |
+------------+ +------------+ +------------+ +-------------------+
Based on BouncyCastle Based on Spring Based on SpringBoot Based on SpringBoot-WebFlux
+
|
v
+------------+ +-------------------+
|bq-boot-base| +-----> | bq-service-auth |
| | | | |
+------------+ | +-------------------+
ased on SpringBoot-Web | Based on SpringSecurity-Authorization-Server
|
|
|
| +-------------------+
+-> | bq-service-biz |
| |
+-------------------+
说明:
bq-encryptor
:基于BouncyCastle
安全框架,已开源 ,加解密介绍
,支持RSA
/AES
/PGP
/SM2
/SM3
/SM4
/SHA-1
/HMAC-SHA256
/SHA-256
/SHA-512
/MD5
等常用加解密算法,并封装好了多种使用场景、做好了为SpringBoot所用的准备;bq-base
:基于Spring框架的基础代码框架,已开源 ,支持json
/redis
/DataSource
/guava
/http
/tcp
/thread
/jasypt
等常用工具API;bq-log
:基于SpringBoot框架的基础日志代码,已开源 ,支持接口Access日志、调用日志、业务操作日志等日志文件持久化,可根据实际情况扩展;bq-boot-root
:基于SpringBoot,已开源 ,但是不包含spring-boot-starter-web
,也不包含spring-boot-starter-webflux
,可通用于servlet
和netty
web容器场景,封装了redis
/http
/定时器
/加密机
/安全管理器
等的自动注入;bq-boot-base
:基于spring-boot-starter-web
(servlet,BIO),已开源 ,提供常规的业务服务基础能力,支持PostgreSQL
/限流
/bq-log
/Web框架
/业务数据加密机加密
等可配置自动注入;bq-service-gateway
:基于spring-boot-starter-webflux
(Netty,NIO),已开源 ,提供了Jwt Token安全校验能力,包括接口完整性校验
/接口数据加密
/Jwt Token合法性校验等;bq-service-auth
:基于spring-security-oauth2-authorization-server
,已开源 ,提供了JwtToken生成和刷新的能力;bq-service-biz
:业务微服务参考样例,已开源 ;
+-------------------+
| Web/App Client |
| |
+-------------------+
|
|
v
+--------------------------------------------------------------------+
| | Based On K8S |
| |1 |
| v |
| +-------------------+ 2 +-------------------+ |
| | bq-service-gateway| +-------> | bq-service-auth | |
| | | | | |
| +-------------------+ +-------------------+ |
| |3 |
| +-------------------------------+ |
| v v |
| +-------------------+ +-------------------+ |
| | bq-service-biz1 | | bq-service-biz2 | |
| | | | | |
| +-------------------+ +-------------------+ |
| |
+--------------------------------------------------------------------+
说明:
bq-service-gateway
:基于SpringCloud-Gateway
,用作JwtToken鉴权,并提供了接口、数据加解密的安全保障能力;bq-service-auth
:基于spring-security-oauth2-authorization-server
,提供了JwtToken生成和刷新的能力;bq-service-biz
:基于spring-boot-starter-web
,业务微服务参考样例;k8s
在上述微服务架构中,承担起了服务注册和服务发现的作用,鉴于k8s
云原生环境构造较为复杂,实际开源的代码时,以Nacos
(为主)/Eureka
做服务注册和服务发现中间件;- 以上所有服务都以docker容器作为载体,确保服务有较好地集群迁移和弹性能力,并能够逐步平滑迁移至k8s的终极目标;
- 逻辑架构不等同于物理架构(部署架构),实际业务部署时,还有DMZ区和内网区,本逻辑架构做了简化处理;
Spring Authorization Server
,2021年底被官方下架了,替代为Spring-Security-OAuth2-Authorization-Server
;Spring-Security-OAuth2-Authorization-Server
只能选用0.2.3
,则必须把SpringBoot/SpringCloud
版本由2.7.x+
/3.1.x+
,降至2.5.x+
/3.0.x+
;Spring Authorization Server
到Spring-Security-OAuth2-Authorization-Server
的扩展点变动非常大,相当于重写一个新的OAuth2服务;Spring-Security-OAuth2-Authorization-Server
主要是通过配置服务生成过滤器而形成一套完整的权限控制体系的,当前的配置能力开发得相对较少,需要采取多种方式灵活地扩展。本人先后采取了反射和非开放的扩展点来达成这一目的,但是相比较而言,后者更优雅一些。通过自定义的加密算法来生成JWK(JSON Web Keys),逻辑如下:
在SpringBoot yaml 中定义RSA2048公钥和私钥:
bq:
auth:
ignoreUrls: /auth/user/*,/${spring.application.name}/monitor/*
channels:
jwt:
serviceId: 6e3c6f31b6894254ae0cd887deaf3318
pubKey: ENC([key]8081087ac1...)
priKey: ENC([key]ac44126761...)
#jwt访问地址
url: /oauth/token
#jwk访问地址
authUrl: /oauth/jwk
#token过期时间(s)
connTimeout: 1800
#刷新token的过期时间(s)
timeout: 3600
新增配置服务
@Slf4j
@Configuration
public class JwtConfigurer
{
@Bean(CommonBootConst.JWT_CHANNEL_CONFIG)
@ConfigurationProperties(prefix = "bq.channels.jwt")
public Channel jwtChannel()
{
return new Channel();
}
@Bean
public JwkMgr jwkMgr(@Qualifier(CommonBootConst.JWT_CHANNEL_CONFIG) Channel channel)
{
return new JwkMgr(channel);
}
@Bean
public JwtMgr jwtMgr(JwkMgr jwkMgr)
{
return new JwtMgr(jwkMgr);
}
}
新增jwk管理器服务JwkMgr :
public final class JwkMgr
{
public JwkMgr(Channel channel)
{
byte[] pubBytes = Hex.decode(channel.getPubKey());
if (null != channel.getPriKey())
{
byte[] priBytes = Hex.decode(channel.getPriKey());
this.priJwk = genRsaKey(priBytes, pubBytes, channel.getServiceId());
}
}
/**
* 生成标准的JWK对象
*
* @return JWK秘钥对象
*/
public JWK getJwk()
{
return this.priJwk;
}
/**
* 生成JWK对象
*
* @param priKey 私钥(非必传时,表示仅需公钥验证)
* @param pubKey 公钥
* @param kid 秘钥id(可重新设置,重启后对所有客户端生效)
* @return JWK秘钥对象
*/
private static RSAKey genRsaKey(byte[] priKey, byte[] pubKey, String kid)
{
RSAPublicKey rsaKey = (RSAPublicKey)ENCRYPTION.toPubKey(pubKey);
RSAKey.Builder builder = new RSAKey.Builder(rsaKey);
if (null != priKey)
{
PrivateKey rsaPriKey = ENCRYPTION.toPriKey(priKey);
builder.privateKey(rsaPriKey);
}
if (null == kid)
{
kid = IdUtil.uuid();
}
return builder.keyID(kid).build();
}
/**
* 加密算法
*/
private final static BaseSingleSignature ENCRYPTION = EncryptionFactory.RSA.createAlgorithm();
/**
* 私钥JWK
*/
private JWK priJwk;
}
扩展oauth2框架中jwk和jwt生成,配置服务为ServerConfigurer :
@Slf4j
@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
public class ServerConfigurer
{
/**
* 注入秘钥管理服务
*
* @param jwkMgr 秘钥管理服务({@link com.biuqu.boot.startup.auth.configure.JwtConfigurer#jwkMgr(Channel)})
* @return 秘钥管理服务
*/
@Bean
public JWKSource<SecurityContext> jwkSource(JwkMgr jwkMgr)
{
JWKSet jwkSet = new JWKSet(jwkMgr.getJwk());
return (jwkSelector, context) -> jwkSelector.select(jwkSet);
}
/**
* 注入 jwt token生成器
*
* @param jwkSource 秘钥上下文
* @return jwt token生成器
*/
@Bean
public JwtGenerator jwtGenerator(JWKSource<SecurityContext> jwkSource)
{
//1.oauth2-server0.2.3匹配的springboot和springboot-security是2.5.12,无法使用NimbusJwtEncoder
JwtGenerator jwtGenerator = new JwtGenerator(new NimbusJwsEncoder(jwkSource));
jwtGenerator.setJwtCustomizer(tokenCustomizer);
return jwtGenerator;
}
@Autowired
private OAuth2TokenCustomizer<JwtEncodingContext> tokenCustomizer;
}
定制的Jwt字段服务JwtCustomizerServiceImpl 如下:
@Slf4j
@Service
public class JwtCustomizerServiceImpl implements OAuth2TokenCustomizer<JwtEncodingContext>
{
@Override
public void customize(JwtEncodingContext context)
{
String clientId = context.getRegisteredClient().getClientId();
ClientResource param = new ClientResource();
param.setAppId(clientId);
ClientResource clientResource = clientService.get(param);
context.getClaims().claim(AuthConst.JWT_RESOURCES, clientResource.getResources());
context.getClaims().claim(JwtClaimNames.JTI, IdUtil.uuid());
context.getClaims().claim(AuthConst.JWT_TYPE, AuthConst.JWT_TYPE_TOKEN);
context.getClaims().claim(OAuth2ParameterNames.TOKEN_TYPE, OAuth2AccessToken.TokenType.BEARER.getValue());
//获取复制出来的jwt body键值对
Map<String, Object> claims = context.getClaims().build().getClaims();
Object sourceType = JwtSourceType.SDK.name();
if (claims.containsKey(AuthConst.JWT_SOURCE_TYPE))
{
sourceType = claims.get(AuthConst.JWT_SOURCE_TYPE);
}
context.getClaims().claim(AuthConst.JWT_SOURCE_TYPE, sourceType);
}
/**
* 注入client信息查询服务
*/
@Autowired
private BaseBizService<ClientResource> clientService;
}
扩展框架的过滤器,新增过滤器管理器服务SecurityFilterMgr :
@Slf4j
@Component
public final class SecurityFilterMgr
{
/**
* 构建定制的过滤器链
*
* @param http 基于请求的安全对象
* @param authManager 安全认证管理对象
* @return 过滤器链(Filter模式)
* @throws Exception 初始化过滤器时的异常
*/
public SecurityFilterChain custom(HttpSecurity http, AuthenticationManager authManager) throws Exception
{
SecurityFilterChain filterChain = http.build();
for (Filter filter : filterChain.getFilters())
{
if (filter instanceof BearerTokenAuthenticationFilter)
{
((BearerTokenAuthenticationFilter)filter).setAuthenticationFailureHandler(authFailureHandler);
}
else if (filter instanceof OAuth2TokenEndpointFilter)
{
//1.加自定义属性
OAuth2TokenEndpointFilter tokenFilter = (OAuth2TokenEndpointFilter)filter;
tokenFilter.setAuthenticationSuccessHandler(authSuccessHandler);
//2.新增刷新转换器
AuthenticationConverter authConverter = new DelegatingAuthenticationConverter(
Arrays.asList(new OAuth2AuthorizationCodeAuthenticationConverter(),
new OAuth2RefreshTokenAuthenticationConverter(),
new OAuth2ClientCredentialsAuthenticationConverter(), refreshAuthConverter));
tokenFilter.setAuthenticationConverter(authConverter);
//3.新增刷新认证器
if (authManager instanceof ProviderManager)
{
ProviderManager providerManager = (ProviderManager)authManager;
List<AuthenticationProvider> providers = providerManager.getProviders();
providers.add(refreshAuthProvider);
}
}
}
return filterChain;
}
/**
* 新增的刷新token认证器
*/
@Autowired
private AuthenticationProvider refreshAuthProvider;
/**
* 新增的刷新token转换器
*/
@Autowired
private AuthenticationConverter refreshAuthConverter;
/**
* 认证成功的处理器
*/
@Autowired
private AuthenticationSuccessHandler authSuccessHandler;
/**
* 认证失败的异常处理器
*/
@Autowired
private AuthenticationFailureHandler authFailureHandler;
}
在认证成功的过滤器中需要保留前面定制的字段,新增主要服务JwtRespMapConverterImpl :
@Slf4j
@Component
public class JwtRespMapConverterImpl extends BaseJwtRespMapConverter
{
@Override
protected ResultCode<JwtResult> toJwtResult(Map<String, Object> parameters)
{
String jwt = parameters.get(OAuth2ParameterNames.ACCESS_TOKEN).toString();
//1.添加扩展字段
JwtToken jwtToken = JwtUtil.getJwtToken(jwt);
parameters.put(AuthConst.JWT_RESOURCES, jwtToken.getResources());
parameters.put(AuthConst.CLIENT_ID, jwtToken.toClientId());
parameters.put(JwtClaimNames.JTI, jwtToken.getJti());
//2.生成刷新token
String refreshJwt = jwtTokenGen.genRefreshJwt(jwt);
parameters.put(OAuth2ParameterNames.REFRESH_TOKEN, refreshJwt);
return super.toJwtResult(parameters);
}
/**
* jwt token生成器
*/
@Autowired
private JwtTokenGen jwtTokenGen;
}
再把上述的扩展点扩展进过滤器的配置服务ServerConfigurer ,上面已列举,此处仅摘要关键方法:
/**
* 注入认证管理器
*
* @param authConf 认证配置
* @return 认证管理器
* @throws Exception 初始化异常
*/
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authConf) throws Exception
{
return authConf.getAuthenticationManager();
}
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain serverChain(HttpSecurity http, AuthenticationManager authManager,
JWKSource<SecurityContext> jwkSource, JwtGenerator jwtGen) throws Exception
{
//1.前后端分离,禁用会话管理和csrf(跨站攻击)
http.sessionManagement().disable();
http.csrf().disable();
//2.添加匿名访问的url(应该包括jwk)
Set<String> anonymous = Sets.newHashSet();
if (!CollectionUtils.isEmpty(ignoreUrls))
{
anonymous.addAll(ignoreUrls);
}
String[] anonUrls = anonymous.toArray(new String[] {});
http.authorizeRequests(registry -> registry.antMatchers(anonUrls).permitAll().anyRequest().authenticated());
//3.设置服务端配置(指定jwt生成器等)
OAuth2AuthorizationServerConfigurer<HttpSecurity> serverConf = new OAuth2AuthorizationServerConfigurer<>();
http.apply(serverConf);
serverConf.tokenGenerator(jwtGen);
//设置认证信息匹配失败的异常
serverConf.clientAuthentication(clientConf -> clientConf.errorResponseHandler(this.failureHandler));
//设置token生成失败的异常
serverConf.tokenEndpoint(tokenConf -> tokenConf.errorResponseHandler(this.failureHandler));
//设置全局处理异常
http.exceptionHandling(exceptionHandler -> exceptionHandler.accessDeniedHandler(this.exceptionHandler));
//4.设置业务请求的jwt解析配置
http.oauth2ResourceServer(resourceConf ->
{
resourceConf.bearerTokenResolver(new DefaultBearerTokenResolver());
OAuth2ResourceServerConfigurer<HttpSecurity>.JwtConfigurer resourceJwtConf = resourceConf.jwt();
resourceJwtConf.decoder(OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource));
//设置资源解析失败的异常(主要是资源带的token解析/认证失败)
resourceConf.accessDeniedHandler(this.exceptionHandler);
});
return filterMgr.custom(http, authManager);
}
综上:
- 通过yaml配置RSA2048公钥和私钥就可以自动生成Jwk秘钥对,并通过框架的扩展点去扩展JwtToken的字段生成;
- JwtToken自定义的字段扩展点里,是通过自定义的服务去实现的,这样就可以完全自主的控制JwtToken字段的生成,但是过程非常繁琐,须好好阅读源码;
@Slf4j
@Service
public class ClientRepositoryServiceImpl implements RegisteredClientRepository
{
@Override
public void save(RegisteredClient registeredClient)
{
}
@Override
public RegisteredClient findById(String id)
{
return null;
}
@Override
public RegisteredClient findByClientId(String clientId)
{
if (StringUtils.isEmpty(clientId))
{
log.error("invalid client id.");
return null;
}
ClientResource clientParam = new ClientResource();
clientParam.setAppId(clientId);
ClientResource clientResource = clientService.get(clientParam);
if (clientResource.isEmpty())
{
log.error("invalid client.");
return null;
}
RegisteredClient.Builder clientBuilder = Oauth2Builder.build(clientId, jwtChannel.getConnTimeout());
clientBuilder.clientSecret(pwdEncoder.encode(clientResource.getAppKey()));
return clientBuilder.build();
}
/**
* jwt配置
*/
@Resource(name = CommonBootConst.JWT_CHANNEL_CONFIG)
private Channel jwtChannel;
/**
* 注入带缓存的业务查询服务
*/
@Autowired
private BaseBizService<ClientResource> clientService;
/**
* 注入密码编码服务
*/
@Autowired
private PasswordEncoder pwdEncoder;
}
@Service
public class ClientResourceServiceImpl extends BaseBizService<ClientResource>
{
@Override
public ClientResource get(ClientResource model)
{
ClientResource client = super.get(model);
if (!client.isEmpty())
{
UrlResource urlParam = new UrlResource();
urlParam.setAppId(model.getAppId());
UrlResource urlResource = urlService.get(urlParam);
client.setResources(urlResource.getUrls());
}
return client;
}
@Override
protected ClientResource queryByKey(String key)
{
ClientResource client = ClientResource.toBean(key);
return dao.get(client);
}
/**
* 注入url服务
*/
@Autowired
private BaseBizService<UrlResource> urlService;
/**
* 注入dao
*/
@Autowired
private BizDao<ClientResource> dao;
}
@Slf4j
@Component
public class JwtTokenGen
{
/**
* 基于当前的jwt对象,生成新的属性Jwt
*
* @param jwt 当前的Jwt
* @return 新Jwt
*/
public String genRefreshJwt(String jwt)
{
try
{
return genRefreshJwt(SignedJWT.parse(jwt));
}
catch (Exception e)
{
log.error("failed to gen refresh token.", e);
}
return null;
}
/**
* 基于当前的jwt对象,生成新的属性Jwt
*
* @param jwt 当前的Jwt
* @return 新Jwt
*/
public String genRefreshJwt(SignedJWT jwt)
{
Map<String, Object> claims = JwtGenUtil.buildClaims(jwt, jwtChannel.getTimeout(), AuthConst.JWT_TYPE_REFRESH);
claims.remove(AuthConst.JWT_RESOURCES);
return JwtGenUtil.genJwt(claims, jwkMgr.getJwk());
}
/**
* jwt配置
*/
@Resource(name = CommonBootConst.JWT_CHANNEL_CONFIG)
private Channel jwtChannel;
/**
* jwk管理器
*/
@Autowired
private JwkMgr jwkMgr;
}
@Slf4j
@Component
public class TokenGatewayFilter implements GlobalFilter, Ordered
{
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain)
{
ServerHttpRequest request = exchange.getRequest();
String url = request.getURI().getPath();
PathMatcher pathMatcher = new AntPathMatcher();
boolean ignore = this.whitelist.stream().anyMatch(pattern -> pathMatcher.match(pattern, url));
log.info("url:{},whitelist:{},result:{}", url, JsonUtil.toJson(this.whitelist), ignore);
if (!ignore)
{
String authorization = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
boolean refreshType = false;
//判断是否为token接口
if (pathMatcher.match(jwtChannel.getUrl(), url))
{
String grantType = request.getQueryParams().getFirst("grant_type");
log.info("url[{}]'s grant type:{}", url, grantType);
refreshType = "jwt_refresh".equalsIgnoreCase(grantType);
//不是刷新token接口调用时,就认定是申请token接口,直接放过
if (!refreshType)
{
return chain.filter(exchange);
}
}
boolean result = jwtMgr.valid(authorization);
log.info("token[{}] valid result:{}", authorization, result);
if (!result)
{
log.error("token auth failed.");
return ServerUtil.writeErr(exchange, ErrCodeEnum.SIGNATURE_ERROR.getCode(), snakeCase);
}
JwtToken jwtToken = JwtUtil.getJwtToken(authorization);
if (null == jwtToken)
{
log.error("parse token failed.");
return ServerUtil.writeErr(exchange, ErrCodeEnum.SIGNATURE_ERROR.getCode(), snakeCase);
}
//token才能请求业务接口
boolean validBizType = !refreshType && !jwtToken.isRefresh();
//刷新token只能请求刷新token
boolean validRefreshType = refreshType && jwtToken.isRefresh();
if (!validBizType && !validRefreshType)
{
log.error("[{}]token type not matched.", url);
return ServerUtil.writeErr(exchange, ErrCodeEnum.SIGNATURE_ERROR.getCode(), snakeCase);
}
}
return chain.filter(exchange);
}
/**
* 是否驼峰式json(默认支持)
*/
@Value("${bq.json.snake-case:true}")
private boolean snakeCase;
/**
* 不用做鉴权的白名单
*/
@Resource(name = GatewayConst.WHITELIST)
private Set<String> whitelist;
/**
* 注入jwt管理器
*/
@Autowired
private PubJwtMgr jwtMgr;
/**
* jwt配置
*/
@Resource(name = CommonBootConst.JWT_CHANNEL_CONFIG)
private Channel jwtChannel;
}
注意:鉴于不确定resources数据量大小,所以此处就没有判断当前请求的url是否在resources列表中。
@Slf4j
@Component
public class SecureAuthGatewayFilter implements GlobalFilter, Ordered
{
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain)
{
//解析出该请求的摘要配置和加密配置
ServerHttpRequest request = exchange.getRequest();
String url = request.getURI().getPath();
//配置转发后,对header中的认证头做校验和解密
String encId = request.getHeaders().getFirst(GatewayConst.HEADER_ENC_ID);
if (authConf.getUrl().equals(url) && this.authConf.needDec())
{
String encAlg = encId;
if (StringUtils.isEmpty(encAlg))
{
encAlg = this.authConf.getDec();
}
String authorization = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
log.info("current auth encrypt[{}][{}]=[{}].", encAlg, authorization,
clientEncryptor.encrypt(encAlg, authorization));
String decAuth = clientEncryptor.decrypt(encAlg, authorization);
if (StringUtils.isEmpty(decAuth))
{
log.error("[{}]decrypt auth header failed.", url);
return ServerUtil.writeErr(exchange, ErrCodeEnum.SIGNATURE_ERROR.getCode(), snakeCase);
}
HttpHeaders headers = new HttpHeaders();
headers.put(HttpHeaders.AUTHORIZATION, Lists.newArrayList(decAuth));
String body = exchange.getAttribute(GatewayConst.BODY_CACHE_KEY);
if (StringUtils.isEmpty(body))
{
body = StringUtils.EMPTY;
}
byte[] data = body.getBytes(StandardCharsets.UTF_8);
request = FluxRequestWrapper.wrap(request, authConf.getRedirect(), headers, data);
}
return chain.filter(exchange.mutate().request(request).build());
}
/**
* 是否驼峰式json(默认支持)
*/
@Value("${bq.json.snake-case:true}")
private boolean snakeCase;
/**
* 认证配置
*/
@Autowired
private EncryptConfig authConf;
/**
* 注入安全服务服务
*/
@Autowired
private ClientSecurity clientEncryptor;
}
@Slf4j
@Component
public class JwtRefreshAuthConverterImpl implements AuthenticationConverter
{
@Override
public Authentication convert(HttpServletRequest request)
{
String uri = request.getRequestURI();
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REFRESH_TOKEN, uri);
// grant_type (REQUIRED)
String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);
if (!AuthConst.REFRESH_GRANT_TYPE.getValue().equals(grantType))
{
log.error("no jwt refresh type find:{}.", uri);
return null;
}
Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication();
// refresh_token (REQUIRED)
String refreshToken = BEARER_JWT_RESOLVER.resolve(request);
String jwtType = AuthConst.JWT_TYPE_TOKEN;
try
{
Map<String, Object> claims = SignedJWT.parse(refreshToken).getJWTClaimsSet().getClaims();
if (claims.containsKey(AuthConst.JWT_TYPE))
{
jwtType = claims.get(AuthConst.JWT_TYPE).toString();
}
}
catch (Exception e)
{
log.error("failed to parse jwt refresh.", e);
throw new OAuth2AuthenticationException(error);
}
if (!AuthConst.JWT_TYPE_REFRESH.equalsIgnoreCase(jwtType))
{
log.error("invalid jwt refresh type");
throw new OAuth2AuthenticationException(error);
}
// scope (OPTIONAL)
String scope = request.getParameter(OAuth2ParameterNames.SCOPE);
if (StringUtils.isEmpty(scope))
{
log.error("no jwt refresh scope find:{}", uri);
throw new OAuth2AuthenticationException(error);
}
Set<String> requestedScopes = Sets.newHashSet(StringUtils.split(scope, " "));
return new OAuth2RefreshTokenAuthenticationToken(refreshToken, clientPrincipal, requestedScopes, null);
}
/**
* jwt token解析器
*/
private static final DefaultBearerTokenResolver BEARER_JWT_RESOLVER = new DefaultBearerTokenResolver();
}
主要参考了OAuth2的源码。
@Slf4j
@Component
public class JwtRefreshAuthProviderImpl implements AuthenticationProvider
{
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException
{
OAuth2RefreshTokenAuthenticationToken refreshTokenAuth = (OAuth2RefreshTokenAuthenticationToken)authentication;
JwtAuthenticationToken principal = getAuthenticatedClient(refreshTokenAuth);
RegisteredClient client = Oauth2Builder.build(principal.getName(), jwtChannel.getConnTimeout()).build();
if (!client.getAuthorizationGrantTypes().contains(AuthConst.REFRESH_GRANT_TYPE))
{
log.error("invalid grant in configs:{}", refreshTokenAuth.getGrantType());
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT);
}
Map<String, Object> addParameters = refreshTokenAuth.getAdditionalParameters();
OAuth2Authorization.Builder authBuilder = Oauth2Builder.build(client);
authBuilder.authorizationGrantType(new AuthorizationGrantType(refreshTokenAuth.getGrantType().getValue()));
authBuilder.attributes(parameters -> parameters.putAll(addParameters));
OAuth2Authorization authorization = authBuilder.build();
Set<String> authorizedScopes = authorization.getAttribute(OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME);
if (!CollectionUtils.containsAny(refreshTokenAuth.getScopes(), authorizedScopes))
{
log.error("invalid scope:{}", refreshTokenAuth.getScopes());
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT);
}
DefaultOAuth2TokenContext.Builder tokenContextBuilder =
DefaultOAuth2TokenContext.builder().registeredClient(client).principal(principal)
.providerContext(ProviderContextHolder.getProviderContext()).authorization(authorization)
.authorizedScopes(authorizedScopes).authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.authorizationGrant(refreshTokenAuth);
OAuth2TokenContext tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.ACCESS_TOKEN).build();
JwtGenerator tokenGenerator = ApplicationContextHolder.getBean(JwtGenerator.class);
OAuth2Token auth2Token = tokenGenerator.generate(tokenContext);
if (auth2Token == null)
{
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.SERVER_ERROR);
}
String jwt = auth2Token.getTokenValue();
Instant issuedAt = auth2Token.getIssuedAt();
Instant expiresAt = auth2Token.getExpiresAt();
OAuth2AccessToken.TokenType tokenType = OAuth2AccessToken.TokenType.BEARER;
OAuth2AccessToken accessToken = new OAuth2AccessToken(tokenType, jwt, issuedAt, expiresAt, authorizedScopes);
String refreshJwt = tokenGen.genRefreshJwt(jwt);
Instant refreshExpiresAt = Instant.ofEpochSecond(issuedAt.getEpochSecond() + jwtChannel.getTimeout());
OAuth2RefreshToken refreshToken = new OAuth2RefreshToken(refreshJwt, issuedAt, refreshExpiresAt);
return new OAuth2AccessTokenAuthenticationToken(client, principal, accessToken, refreshToken, addParameters);
}
@Override
public boolean supports(Class<?> authentication)
{
return OAuth2RefreshTokenAuthenticationToken.class.isAssignableFrom(authentication);
}
/**
* 获取认证通过的token对象
*
* @param authentication 认证对象
* @return 认证后的token对象
* @throws OAuth2AuthenticationException 认证失败异常
*/
private static JwtAuthenticationToken getAuthenticatedClient(OAuth2RefreshTokenAuthenticationToken authentication)
throws OAuth2AuthenticationException
{
JwtAuthenticationToken clientPrincipal = null;
Object principal = authentication.getPrincipal();
if (principal instanceof JwtAuthenticationToken)
{
clientPrincipal = (JwtAuthenticationToken)principal;
}
if (clientPrincipal != null && clientPrincipal.isAuthenticated())
{
return clientPrincipal;
}
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT);
}
@Autowired
private JwtTokenGen tokenGen;
/**
* jwt配置
*/
@Resource(name = CommonBootConst.JWT_CHANNEL_CONFIG)
private Channel jwtChannel;
}
主要参考并综合了OAuth2几种类型的实现,并需要新增一个AuthorizationGrantType:
jwt_refresh
。
@Slf4j
public final class JwtGenUtil
{
/**
* 生成JwtToken base64字符串
*
* @param claims jwt body集合
* @param jwk 秘钥
* @return JwtToken base64字符串
*/
public static String genJwt(Map<String, Object> claims, JWK jwk)
{
try
{
JWTClaimsSet jwtClaimsSet = JWTClaimsSet.parse(claims);
return genJwt(jwtClaimsSet, jwk);
}
catch (Exception e)
{
log.error("failed to gen jwt token.", e);
}
return null;
}
/**
* 生成JwtToken base64字符串
*
* @param jwtClaimsSet jwt body集合
* @param jwk 秘钥
* @return JwtToken base64字符串
*/
public static String genJwt(JWTClaimsSet jwtClaimsSet, JWK jwk)
{
try
{
JWSSigner jwsSigner = SIGNER_FACTORY.createJWSSigner(jwk);
SignedJWT signedJwt = new SignedJWT(new JWSHeader(JWSAlgorithm.RS256), jwtClaimsSet);
signedJwt.sign(jwsSigner);
return signedJwt.serialize();
}
catch (Exception e)
{
log.error("failed to gen jwt token.", e);
}
return null;
}
/**
* 基于现有的Jwt构建新的Jwt claims
*
* @param jwtToken jwt参数
* @return 新的Jwt claims
*/
public static Map<String, Object> buildClaims(JwtToken jwtToken)
{
Map<String, Object> claims = buildClaims(jwtToken.getExp(), jwtToken.getJwtType());
claims.put(JwtClaimNames.SUB, jwtToken.getSub());
claims.put(JwtClaimNames.AUD, jwtToken.getAud());
claims.put(JwtClaimNames.ISS, StringUtils.EMPTY);
claims.put(AuthConst.JWT_RESOURCES, jwtToken.getResources());
return claims;
}
/**
* 基于现有的Jwt构建新的Jwt claims
*
* @param signedJwt 签名后的jwt
* @param expire 有效时长(s)
* @param jwtType jwt类型(token/refresh)
* @return 新的Jwt claims
*/
public static Map<String, Object> buildClaims(SignedJWT signedJwt, long expire, String jwtType)
{
Map<String, Object> claims = Maps.newHashMap();
try
{
claims.putAll(signedJwt.getJWTClaimsSet().getClaims());
Map<String, Object> newClaims = buildClaims(expire, jwtType);
claims.putAll(newClaims);
}
catch (Exception e)
{
log.error("failed to gen jwt token.", e);
}
return claims;
}
/**
* 基于现有的Jwt构建新的Jwt claims
*
* @param expire 有效时长(s)
* @param jwtType jwt类型(token/refresh)
* @return 新的Jwt claims
*/
public static Map<String, Object> buildClaims(long expire, String jwtType)
{
Map<String, Object> claims = Maps.newHashMap();
long validTime = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis());
long expireTime = validTime + expire;
claims.put(JwtClaimNames.IAT, validTime);
claims.put(JwtClaimNames.NBF, validTime);
claims.put(JwtClaimNames.EXP, expireTime);
claims.put(JwtClaimNames.JTI, IdUtil.uuid());
claims.put(OAuth2ParameterNames.TOKEN_TYPE, OAuth2AccessToken.TokenType.BEARER.getValue());
claims.put(AuthConst.JWT_TYPE, jwtType);
return claims;
}
private JwtGenUtil()
{
}
/**
* 默认的签名工厂
*/
private static final JWSSignerFactory SIGNER_FACTORY = new DefaultJWSSignerFactory();
}
spring-security-oauth2-authorization-server
官方给出的demo中有登录和OAuth2完全分离的例子;注意:
- 只有带有页面时,才需要通过OAuth2服务继续查询资源信息,才需要分布式会话管理,否则直接通过网关就可以鉴权完成,再也用不上OAuth2服务了;
- OAuth2服务负责认证,网关负责鉴权,不代表OAuth2服务就不能鉴权,这只是我们大部分场景上的设计,目的是提升效率。实际上,OAuth2服务中,无论是刷新JwtToken接口还是资源(权限、用户、菜单等)获取接口,都先用通过OAuth2服务的鉴权;
spring-session-data-redis
,参见Spring集成redis实现分布式会话。
<dependency>
<groupId>org.springframework.sessiongroupId>
<artifactId>spring-session-data-redisartifactId>
dependency>
#session超时时间设置为1000秒
server:
session:
timeout: 1000
#设置session的存储方式,none为存在本地内存中,redis为存在redis中
spring:
session:
store-type: redis
#namespace用于区分不同应用的分布式session
redis:
namespace: oauth2
#session更新到redis的模式,分为on_save和immediate,on_save是当执行执行完响应以后才将数据同步到redis,immediate是在使用session的set等操作时就同步将数据更新到redis,建议使用on_save
flush-mode: on_save
定制的Jwt字段服务
章节中已经介绍了,目前的接口服务的JWT_SOURCE_TYPE
是JwtSourceType.SDK.name()
。spring-security-oauth2-authorization-server
版本,则要替换JDK,升级Spring版本,升级SpringBoot版本,升级SpringCloud版本,升级Nacos等其它三方件版本……工作量巨大,而且还可能导致功能不正常,升级风险巨高;0.2.3
到1.1.0
,就是一个框架从不成熟走向成熟了。欣慰至极。