前言
本文是我在工作上的需求衍生,因为案例比较经典,遂加以记录,希望以后能对其他类似工作具有参考性!
需求描述
在用户购买产品套餐后,会拥有调用相关接口的权限,此时,权限校验的机制是怎样的呢?如何设计与实现会比较合理?
需求点梳理
- 获取用户权限的实现原理是什么?
首先需要在开放平台上获取用户的所有订单详情,判断用户的购买套餐是否是对应的产品,然后需要判断产品的使用期限是否>0,如果均满足条件,则判断用户权限是true,如果用户的所有订单详情均不满足,则为false;如果查询订单为空,证明用户从没有下过单,为false; - 由于调用的接口可能非常多,均需要权限校验,我们如何使用简化代码来满足多个接口的校验?
可以添加自定义注解与拦截器,去校验用户是否具有权限; - 为了减少用户频繁调用其他平台服务,我们可以将校验结果设定一个时间期限,缓存到redis中去;
- Redis中的记录什么时候需要消失呢?
需要考虑到,除了过期以外,在用户去往订阅页面下单的时候也需要将记录清除,否则,会发生用户即使下过单,但也还是无法获取到权限的事故!
方案设计
代码实现
- 构建权限校验注解
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE,ElementType.METHOD})
public @interface VisualToBuyToken {
// 校验token
boolean validate() default true;
}
- 在拦截器中调用校验权限方法
@Component
@Slf4j
@NonNullApi
public class VisualAuthorizationInterceptor extends HandlerInterceptorAdapter {
@CloudReference(service="business")
private SubscribeService subscribeService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
VisualToBuyToken annotation;
if (handler instanceof HandlerMethod) {
//先获取方法上的注解
annotation = ((HandlerMethod) handler).getMethodAnnotation(VisualToBuyToken.class);
//如果方法没有注解,则去类上去获取 如果方法有则使用方法的注解,方法的注解优先级高于类的注解
if (annotation == null) {
annotation = ((HandlerMethod) handler).getBeanType().getAnnotation(VisualToBuyToken.class);
}
}
// 如果请求的不是方法 则直接跳过当前拦截器
else {
return true;
}
//没有声明需要权限,或者声明不验证权限
if (annotation == null || !annotation.validate()) {
return true;
}
......
//校验用户是否有权限
if (!subscribeService.verVisualToBuy(loginUser)) {
throw new BaseBizException(ResponseEnum.ERROR_NO_VISUAL_AUTH.getErrorCode(), ResponseEnum.ERROR_NO_VISUAL_AUTH.getErrorInfo());
}
......
return true;
}
}
- verVisualToBuy方法校验权限
@Override
public Boolean verVisualToBuy(LoginUser loginUser) {
// 首先从redis中获取
Boolean visualToBuyAuth = LightDataConstant.HS_CACHE.get(LightDataConstant.VISUAL_TO_BUY + loginUser.getAuthId() , Boolean.class);
if (visualToBuyAuth != null) {
return visualToBuyAuth;
}
// 若redis中为空,执行查询逻辑并缓存到redis中去,缓存时间设置为1小时
visualToBuyAuth = getVisualToBuy(loginUser);
LightDataConstant.HS_CACHE.put(LightDataConstant.VISUAL_TO_BUY + loginUser.getAuthId(), visualToBuyAuth, 3600);
return visualToBuyAuth;
}
- 在订阅时开启多线程删除缓存,这是因为需要不影响其他订阅过程的展开~,需要注意,删除之前首先判断是否存在记录,若没有,则不删除直接跳过
// 开启多线程进行redis清空缓存,不妨碍下面的展开
poolTaskExecutor.execute(() -> {
// 清理redis缓存
if ("可视化套餐".equals(addOrPayOrderOutSideReq.getSpan())) {
subscribeService.deleteVisualToBuyRecord(loginUser);
}
});
@Override
public void deleteVisualToBuyRecord(LoginUser loginUser) {
try {
if (Objects.isNull(LightDataConstant.HS_CACHE.get(LightDataConstant.VISUAL_TO_BUY + loginUser.getAuthId() , Boolean.class))) {
return;
}
// 清理redis缓存
LightDataConstant.HS_CACHE.evict(LightDataConstant.VISUAL_TO_BUY + loginUser.getAuthId());
} catch (Exception e) {
log.error("[subscribe] 可视化权限删除缓存记录 deleteVisualToBuyRecord报错:{}", e.getMessage());
}
}
自测与思考
在后续的自测中,会发现,基本能满足权限校验的需求;但思考一个问题:当并发量足够大的时候,在订单提交时开启多线程删除缓存,会不会有时间上的延迟导致用户下订单后立马去访问接口,从而出现权限校验仍然报错的情况?
情景描述: 有两个人同时操作,一个人购买套餐,另一个人立马去请求获取订阅页面,有可能存在订阅前请求调用,然后订阅完以后权限依然没有的情况;如下图所示:
在这种情况主要是因为:当前下单订阅时的并发量过大,在开启多线程清除redis缓存时可能还未排队等到,用户就去调用有图页面了,造成权限获取仍然为false的情况;
另外,还有一种情况也具有突然性,即虽然恒有数这边下单成功,但开放平台那边用户记录更新不及时,也会造成权限获取仍然为false的情况;
解决方案:可以尝试在本项目与开发平台这边搭建一个消息队列,当下单时将消息放入队列中,当调用权限时去队列中检查有无对应数据即可;