一个企业级的项目,权限系统是必不可少的,Java中Spring Security
和Shiro
都是非常不错的选择,但是经过技术调研和需求调研,发现这2者都不能够100%的满足我们的需求,同时也没有想象中的轻量。
没有轮子或者轮子不好,那我们就来造一个轮子吧。
RBAC(Role-Based Access Control)
基于角色的访问控制1:1
的关系,也可以是1:N
的关系,有一个用户-角色
中间表1:N
,有一个角色-权限
中间表这里我们需要思考一个点:权限到底如何表示(定位)?
Controller
层的方法上的URL
来定义一个权限。因为是基于URL
该方式通常可采用Filter
实现鉴权Controller
层方法上的注解
来定义一个权限。因为该方法需要获取到方法上的注解,所以通常由基于反射的Interceptor
实现鉴权Filter 通常的实现依赖的是
FilterChain
,基于函数回调
实现Interceptor通常是通过
反射
来实现的上述2者都体现了AOP的思想噢...
在上一个项目中,我们采用的是第一种URL
来定义一个权限,但是发现了如下几个问题:
URL
不够语义化,运营人员不太明白。比如基于REST
规范的Get /student
和POST /student
,他们不明白这个表示的到底是什么功能权限!URL
的写法可能是Request
也可能是PathVariable
,这些搅和在一起,让人困惑。URL
一旦同角色绑定过后,不能轻易修改,否则之前的授权就需要重新绑定当然上面的这些都有办法去解决,但是处理完成后,我们认为这些处理方式不够统一,为了解决这些问题,我们结合Swagger
定制了一套权限管理系统。
基于Interceptor
的实现,很多第三方框架都是定义了一些注解,比如这样的:
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface AuthRequired {
//一些必要参数...
}
但是在我看来,能够复用已经存在于框架中的东西的时候,还是要利用起来的。
同时我们公司要求对外的方法需要通过Swagger
来注解,方便前端开发人员明确方法含义和方便生成文档,比如这样的示例:
@ApiOperation(value = "【查询】用户统计时候可用的省的",nickname = "L029-02")
@GetMapping("/pv-data/province")
public ApiRet<IPage<String>> findProvince(PVProvincePage provincePage) {
IPage<String> result = userUseViewService.findProvince(provincePage);
return new ApiRet<>(result);
}
仔细一看,swagger
的注解ApiOperation
描述的不就是我们想要的吗?
value
注解,中文汉字语义明确,而且可以在后期随时变更nickname
来定义id
,业务上保证全局唯一,通过该值同角色绑定。这不就是我们想要的,最为核心的,能够准确描述一个能够让运维人员看懂的权限吗!?
假如按照上述的规定定义权限功能,当应用启动的时候,通过某种方式扫描全局的Controller
中的所有public
且带有ApiOperation
的方法,这就可以获取当前系统的所有需要处理的权限了。
获取权限后你是缓存到内存中还是将扫描到的集合入库,都是可以的,这里我将我们的扫描ApiOperation
核心API
提一下
API
的代码大略如下所示 /**
* 扫描基础包下的方法,并将其映射为
*
* @param basePackage 基础扫描包
* @return 权限集合
*/
public static Set<OperationInfo> scan(String basePackage) {
//ClassScanner 采用的是hutool的,性能没问题
Set<Class<?>> classes = ClassScanner.scanPackageByAnnotation(basePackage, RestController.class);
Set<OperationInfo> result = Optional.ofNullable(classes)
.orElse(new HashSet<>(0))
.stream()
.map(ApiOperationScanner::mapToOperationInfo) //将Swagger注解映射为权限实体
.filter(CollectionUtil::isNotEmpty)
.flatMap(Set::stream)
.collect(Collectors.toSet());
return result;
}
获取到对应的后,如果要入库,需要对比下之前的权限和现在的权限的差异
上述的这些细节都需要考虑到,这里就不详诉了,请结合你们的业务需求自行分析
基于RBAC
的思路,权限是绑定在角色上的,所以你只需要将某个角色具有的权限绑定在一起,入库即可。当需要鉴权时候,查找该用户的对应的角色是否预备这个方法即可,我们来看看下述代码:
创建一个拦截器UEInterceptor
@Component
public class UEInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
//...省略部分代码...
//获取到当前拦截的方法的注解
ApiOperation operation = method.getAnnotation(ApiOperation.class);
//获取token
String token = request.getHeader(USER_TOKEN);
//判定这个token是否存在
boolean exists = checkTokenExists(userId, token);
if (!exists) {
throw new AccountLoseEffectException(token);
}
//通过token获取到userId的获取
Long userId = tokenManager.parseUserId(token);
//判定当前用户的角色是否包含功能
boolen flag = userHasAuth(userId,operation)
if(!flag){
throw new PermissionMissException();
}
//通过ThreadLocal缓存数据用户的数据信息
UserContext.storeUser(new UserInfo(userId, roleId, userCore.getLoginName()));
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
@Nullable Exception ex) throws Exception {
//防止内存泄露,所以请求完该方法后,清空数据
UserContext.clear();
}
}
关键实现其中的2个方法:
preHandle
就是在真正执行方法前,进行某些操作。上述方法主要是:
token
是否存在,是否合法token
对应的用户的角色是否具备这个功能ThreadLocal
中缓存用户信息afterCompletion
就是方法执行完成后,做的操作
ThreadLocal
的值,避免内存泄露上述的拦截器如果要生效,还需要实现接口WebMvcConfigure
来进行如下设置:
@Configuration
public class UEInterceptorConfig implements WebMvcConfigurer {
@Autowired
private UEInterceptor ueInterceptor;
@Value("${ue.interceptor.include:/**}")
private String[] includePaths;
@Value("${ue.interceptor.exclude}")
private String[] excludePaths;
//添加拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(ueInterceptor).order(Integer.MIN_VALUE)
.addPathPatterns(includePaths).excludePathPatterns(excludePaths);
}
}
includePaths
可以默认为/**
表示所有的都拦截excluedePaths
为需要排除的拦截URI
,比如登录、获取验证码、静态资源请求等order(Integer.MIN_VALUE)
来定义顺序本文阐述了下述几个要点:
Filter
改为了Interceptr
Swagger
来定义一个权限(功能)
的ClassScaner
扫描出所有的对应的方法token
判定、权限判定
、用户信息保存在ThreadLocal
,以及方法执行完成后清理ThreadLocal
include
和exclude
上文中又不少细节同项目有关,根据公司规定,我不能贴太多代码,所以仅仅提了下思路,请结合自己的理解去完善和实践,不要怕做错,自己尝试的绝对比别人教你的印象深刻。