Quiet 项目简介:juejin.cn/post/717122…
上篇:Dubbo + Spring Security Oauth2 如何优雅解决服务提供者无法获取当前用户(上)
前言
在上篇中我们搞明白了方法 SecurityContextHolder.getContext().getAuthentication()
获取用户信息的整个过程,并且在 Dubbo 调用的时候,服务提供者无法通过该方法获取用户信息的原因,这篇我们就解决这个问题。
切入点
既然用的是 Dubbo 发起的请求,那么是否可以在发起请求和受理请求的时候,对请求进行处理,像这种开源项目,其提供的扩展点是很多的,在 Dubbo 中我们可以通过 自定义 Filter 在服务消费者和服务提供者之间实现数据的添加和获取,其原理类似 Java SPI。
解决方案
方案一:传递用户信息
既然服务提供者无法获取当前用户信息,那我们可以在调用的时候把整个用户信息都传递过去,然后再设置到 SecurityContext
中,这样服务提供者就能获取当前用户信息了。这个方案是可以解决问题的,但是非常不好,因为每个用户的用户信息数据量是不固定的,每个用户拥有的权限、角色等信息都是不一样的,传递的数据量不可控,而且会消耗大量的网络资源。使用这种方案还需要解决序列化的问题,因为这个方案不是最佳的,这里就不展开了。
方案二:自己定义一个 key,将用户信息存到 Redis
在服务消费者发起请求的时候,自己定义一个 Redis key,将用户信息存储到 Redis,然后再将这个 key 传递给服务提供者,服务提供者再去 Redis 获取用户信息。这个方案是上篇遇到的那个面试官提出的,面试的时候没仔细想这个方案的好坏,等面试结束的时候发现这个方案还是有问题的。
首先呢,Redis 已经存有一份用户信息了,再存一份,属实没必要,而且还需要维护两份用户信息,保持两份用户信息的一致。再者,这个 Redis key 的过期时间要怎么设置?设置一个固定的时间吗?这个是不行的,因为在整个系统中,用户有一个统一的 token 过期时间,如果自定义的 Redis key 时间是固定的,就会出现 token 已经过期,但是服务提供者无法知晓当前用户已经过期,因为在 Redis key 还未过期的时候它仍然能获取用户信息,这已经破坏了系统的整体性,那么就得动态去计算这个 Redis key 的过期时间,屎山就是这么堆起来的~
方案三:直接使用 token
直接使用 token,服务消费者和服务提供者以及整个系统都共用一个用户信息,方案一和方案二的所有问题也就不存在了。在上篇中我们已经知晓如何通过一个 token 获取当前用户信息,那么我们可以从 request 中获取并直接传递 token,在服务提供者处复现获取用户信息的过程。同时,服务提供者什么时候获取用户信息也是需要考虑的,为什么?因为服务消费者调用服务提供者的接口时,服务提供者并不一定需要当前用户信息,如果在请求到达服务提供者的 Filter 时就马上去获取用户信息,设置到 SecurityContext
中,获取的用户信息对服务提供者的接口来说并不一定是有用的(可能大部分接口的业务逻辑是不需要用户信息的),那么这就会造成大量的资源浪费。
也就是说,除了解决如何使用 Filter 传递 token 之外,我们还需要解决两个问题:
- 如何通过 token 重新获取当前用户信息
- 如何实现服务提供者在需要的时候才真正去获取用户信息
实现步骤
传递 token
在 Dubbo 中如何自定义 Filter:调用拦截扩展
创建配置文件
在文件夹 src/main/java/resources/META-INF/dubbo
下创建文件名为 org.apache.dubbo.rpc.Filter
的纯文本文件,文件内容为:
token-value-consumer=com.gitee.quiet.service.dubbo.filter.consumer.AccessTokenValueFilter
token-value-provider=com.gitee.quiet.service.dubbo.filter.provider.AccessTokenValueFilter
注册这两个 Filter
dubbo:
consumer:
filter: token-value-consumer
provider:
filter: token-value-provider
定义服务消费者和服务提供者的 Filter
在服务消费者发起调用的时候获取 requset 中的 token 值
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
String tokenValue = (String)((ServletRequestAttributes) requestAttributes)
.getRequest()
// 这个 key 在上篇有提到
.getAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE);
这个地方会有个问题,如果是服务 A
调用服务 B
,服务 B
调用服务 C
,这时候服务 B
是服务消费者,获取的 requestAttributes
是 null
,这时就无法获取 token 了。
分析并解决:服务 B
其实是有 token 的,只是在调用服务 C
的时候无法在 Filter 处获取而已,此时我们可以借助 ThreadLocal:
public static final ThreadLocal<String> USER_TOKEN = ThreadLocal.withInitial(() -> "");
通过 token 获取用户信息
在上篇中我们知道可以使用 token 在 tokenStore
中获取用户信息,tokenStore
是我们注入的一个 Bean,那么我们就可以从 Spring 容器中获取这个 Bean:
TokenStore tokenStore = SpringUtil.getBean(TokenStore.class);
OAuth2Authentication authentication = tokenStore.readAuthentication(tokenValue);
服务提供者如何在需要用户信息的时候才去获取
为了不影响 web 请求获取用户信息的方式,在使用 Dubbo 调用的时候,服务提供者尽可能考虑通过与 web 请求获取用户信息相同的方式获取当前用户信息,也就是通过 SecurityContextHolder.getContext().getAuthentication()
方法获取用户信息,如果实在无法实现,再考虑自定义一种能满足这两种情况下都能获取用户信息的方式(优雅永不过时~)。
在上篇中我们分析了 SecurityContextHolder
,在这个类里面提供了 setContext
方法设置 SecurityContext
,再看下 org.springframework.security.core.context.SecurityContext
,这是一个接口,我们可以自己定义实现类,在自定义的实现类中,在调用 getAuthentication
的时候才去获取用户信息,再将这个自定义类的实例作为形参调用 setContext
,就可以实现服务提供者在需要用户信息的时候才去获取用户信息:
public class QuietSecurityContext implements SecurityContext {
private final String tokenValue;
private final SecurityContext securityContext = SecurityContextHolder.getContext();
public QuietSecurityContext(@NotBlank String tokenValue) {
this.tokenValue = tokenValue;
}
@Override
public Authentication getAuthentication() {
if (securityContext.getAuthentication() != null) {
return securityContext.getAuthentication();
}
TokenStore tokenStore = SpringUtil.getBean(TokenStore.class);
OAuth2Authentication authentication = tokenStore.readAuthentication(tokenValue);
this.setAuthentication(authentication);
return securityContext.getAuthentication();
}
@Override
public void setAuthentication(Authentication authentication) {
securityContext.setAuthentication(authentication);
}
}
这里用到了设计模式中的装饰器模式,所以不能说设计模式在工作中没用,只是没遇到特定的场景!
Filter 实现
com.gitee.quiet.service.dubbo.filter.consumer.AccessTokenValueFilter
@Activate(group = CommonConstants.CONSUMER)
public class AccessTokenValueFilter implements Filter {
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
String tokenValue;
if (requestAttributes != null) {
tokenValue =
(String)
((ServletRequestAttributes) requestAttributes)
.getRequest()
.getAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE);
} else {
tokenValue = DubboThreadLocal.USER_TOKEN.get();
}
if (StringUtils.isNotBlank(tokenValue)) {
invocation.setAttachment(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, tokenValue);
}
return invoker.invoke(invocation);
}
}
com.gitee.quiet.service.dubbo.filter.provider.AccessTokenValueFilter
@Activate(group = CommonConstants.PROVIDER)
public class AccessTokenValueFilter implements Filter {
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
String tokenValue = invocation.getAttachment(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE);
if (StringUtils.isNotBlank(tokenValue)) {
DubboThreadLocal.USER_TOKEN.set(tokenValue);
SecurityContextHolder.setContext(new QuietSecurityContext(tokenValue));
}
try {
return invoker.invoke(invocation);
} finally {
if (StringUtils.isNotBlank(tokenValue)) {
SecurityContextHolder.clearContext();
DubboThreadLocal.USER_TOKEN.remove();
}
}
}
}
更详细的代码可以看 Quiet 项目中的
quiet-spring-boot-starters/quiet-service-spring-boot-starter/src/main/java/com/gitee/quiet/service/dubbo/filter
结语
至此,Dubbo + Spring Security Oauth2 解决服务提供者无法获取当前用户的方案还算优雅吧