项目搭建1:基于切面的接口访问控制

前言

产品有这样的一个需求:在一段时间内,禁止用户修改个人信息。

项目中之前的写法是:

1、在一个数据表中,记录要禁止的操作类型,比如,修改用户信息

2、在相关接口中,查询该表的数据,判断状态是否处于“生效”状态

3、在指定时间段内,修改数据表中记录的状态值

这种做法存在以下两个问题:

  • 权限控制代码分在业务逻辑中,不便于后续维护
  • 每次需要在指定时间段内修改记录状态值,有时还需要在深夜来修改状态

优化方案也很简单:

  • 禁止用户修改个人信息,是一种接口访问控制,最佳实践是使用切面,以接口作切点,在切面控制接口的访问;
  • 使用“时间”代替“状态”,如果当前请求在指定的时间段内,就禁止该行为,这样可以提前设置好时间,不用在深夜去修改状态

下面介绍该方案的实现。

该方案的实现很简单,实现该方案的主要目的是,搭建一个自己的项目,不断丰富项目功能,并在项目上做一些技术实践。

项目结构

目标是搭建一个微服务项目wpm(work plan manager,工作计划管理),现阶段还是一个“多模块”的单体应用,主要分为四层,如下图所示:

image-20210924192603371

1、业务聚合层:项目的对业务入口,主要是 APP 模块和 Admin 模块

2、业务逻辑层:提供诸如“用户、部门、资源”等业务服务

3、公共服务层:提供诸如“权限校验、统计、消息推送”等基础功能服务

4、基础依赖层:各个模块的依赖组件,目前是以模块的形式存在,后续应该是以组件的形式存在

项目代码的组织结构与上图相似,如下图所示:

image-20210924193030119

目前只搭建了“app、user、authority、common”模块,因为目前只有 app 模块对外提供接口,所以 user、authority 模块只是作为 app 模块的依赖模块而存在,并不是作为一个服务而存在。

请求过程

根据我们在”前言“中的方案,用户发起”更新用户信息“的请求,过程如下:

  • 请求进入切面中,在切面里查询数据库配置
  • 根据配置数据检查是否可以执行请求
  • 如果检查通过,就继续执行,否则直接返回拒绝结果

请求过程和时序图如下所示:

image-20210924200244533

请求过程简图

image-20210924200437847

请求时序图

代码实现

创建数据表

 CREATE TABLE `common_authority` (
   `id` int(11) NOT NULL AUTO_INCREMENT,
   `operate_type` varchar(30) NOT NULL COMMENT '操作类型',
   `start_time` datetime DEFAULT NULL COMMENT '开始生效时间',
   `end_time` datetime DEFAULT NULL COMMENT '生效结束时间',
   `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
   `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
   PRIMARY KEY (`id`),
   UNIQUE KEY `uk_operate_type` (`operate_type`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
复制代码

注解类

 @Target(ElementType.METHOD)
 @Retention(RetentionPolicy.RUNTIME)
 public @interface EnableCommonAuthority {
 ​
     /**
      * 默认的操作类型,默认是修改用户信息
      */
     String operateType() default ICommonAuthConstant.OperateType.CHANGE_USER_INFO;
 }
复制代码

切面

在进入切点前执行检查。

 @Aspect
 @Component
 public class CommonAuthorityAspect {
 ​
     @Resource
     private LoadingCache<String, CommonAuthority> commonAuthorityCache;
 ​
     @Pointcut("@annotation(com.shimengjie.wpm.authority.domain.model.commonauthority.annotation.EnableCommonAuthority)")
     public void pointcut() {
 ​
     }
 ​
     @Before(value = "pointcut()")
     public void before(JoinPoint pjp) {
         EnableCommonAuthority annotation = BeanUtils.findAnnotation(pjp, EnableCommonAuthority.class);
         if (null == annotation) {
             return;
         }
         String operateType = annotation.operateType();
         // 这里要调用 get 方法,才会执行 cacheLoader 加载数据
         CommonAuthority commonAuthority = commonAuthorityCache.get(operateType);
         if (commonAuthority != null && !commonAuthority.isAllow()) {
             throw new AccessDeniedException();
         }
     }
 }
复制代码

缓存

为了减少每次查询数据库的时间,使用本地缓存 LoadingCache(目前是单例,暂时使用本地缓存)缓存要查询的数据:

 // cache loader 
 @Component
 public class CommonAuthorityCacheLoader implements CacheLoader<String, CommonAuthority> {
 ​
     @Resource
     private MybatisCommonAuthorityRepository mybatisCommonAuthorityRepository;
 ​
     @Override
     public CommonAuthority load(String key) {
         return mybatisCommonAuthorityRepository.queryOfOperateType(key);
     }
 ​
 }
 ​
 // 配置缓存 bean
 @Configuration
 public class CacheConfig {
 ​
     @Bean
     public LoadingCache<String, CommonAuthority> commonAuthorityCache(CommonAuthorityCacheLoader cacheLoader) {
         return Caffeine.newBuilder()
                 .expireAfterWrite(5, TimeUnit.MINUTES)
                 .build(cacheLoader);
     }
 }
复制代码

接口

给 app 模块中的“修改用户信息”接口,添加注解:

 @Slf4j
 @RestController
 @RequestMapping(value = "api/wpm/app/user", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
 public class UserController {
 ​
     @Resource
     private UserAdapter userAdapter;
 ​
     @ApiOperation("修改用户信息")
     @PutMapping("/{id}")
     @EnableCommonAuthority
     public AbstractResponse updateById(@PathVariable("id") Long id, @RequestBody UserUpdateCommand command) {
         userAdapter.updateById(id, command);
         return new AbstractResponse();
     }
 }
复制代码

测试

在数据表新增数据,设置当前时间(2021-09-24 20:28:00)处于禁止时间范围内:

 INSERT INTO common_authority (operate_type,start_time,end_time) VALUES
      ('changeUserInfo','2021-09-24 20:00:00','2021-09-24 23:00:00');
复制代码

启动 app 应用,使用 postman 执行接口,在切面里打上断点,结果如下:

screenshot-20210924-203226

至此,一个简单的“基于切面的接口访问控制“功能就实现了。

总结

使用切面实现了接口访问的控制,通过对这个简单功能的优化,搭建了一个项目雏形,后续会不断丰富项目功能。

项目地址:github.com/ShiMengjie/…

原文地址:mp.weixin.qq.com/s/dyR8pKO6l…