前言
产品有这样的一个需求:在一段时间内,禁止用户修改个人信息。
项目中之前的写法是:
1、在一个数据表中,记录要禁止的操作类型,比如,修改用户信息
2、在相关接口中,查询该表的数据,判断状态是否处于“生效”状态
3、在指定时间段内,修改数据表中记录的状态值
这种做法存在以下两个问题:
- 权限控制代码分在业务逻辑中,不便于后续维护
- 每次需要在指定时间段内修改记录状态值,有时还需要在深夜来修改状态
优化方案也很简单:
- 禁止用户修改个人信息,是一种接口访问控制,最佳实践是使用切面,以接口作切点,在切面控制接口的访问;
- 使用“时间”代替“状态”,如果当前请求在指定的时间段内,就禁止该行为,这样可以提前设置好时间,不用在深夜去修改状态
下面介绍该方案的实现。
该方案的实现很简单,实现该方案的主要目的是,搭建一个自己的项目,不断丰富项目功能,并在项目上做一些技术实践。
项目结构
目标是搭建一个微服务项目wpm(work plan manager,工作计划管理),现阶段还是一个“多模块”的单体应用,主要分为四层,如下图所示:
1、业务聚合层:项目的对业务入口,主要是 APP 模块和 Admin 模块
2、业务逻辑层:提供诸如“用户、部门、资源”等业务服务
3、公共服务层:提供诸如“权限校验、统计、消息推送”等基础功能服务
4、基础依赖层:各个模块的依赖组件,目前是以模块的形式存在,后续应该是以组件的形式存在
项目代码的组织结构与上图相似,如下图所示:
目前只搭建了“app、user、authority、common”模块,因为目前只有 app 模块对外提供接口,所以 user、authority 模块只是作为 app 模块的依赖模块而存在,并不是作为一个服务而存在。
请求过程
根据我们在”前言“中的方案,用户发起”更新用户信息“的请求,过程如下:
- 请求进入切面中,在切面里查询数据库配置
- 根据配置数据检查是否可以执行请求
- 如果检查通过,就继续执行,否则直接返回拒绝结果
请求过程和时序图如下所示:
请求过程简图
请求时序图
代码实现
创建数据表
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 执行接口,在切面里打上断点,结果如下:
至此,一个简单的“基于切面的接口访问控制“功能就实现了。
总结
使用切面实现了接口访问的控制,通过对这个简单功能的优化,搭建了一个项目雏形,后续会不断丰富项目功能。




近期评论