咱们的业务系统使用了一段时间后,用户的角色类型愈来愈多,这时候不一样类型的用户可使用不一样功能,看见不一样数据的需求就变得愈来愈迫切。 如何设计一个可扩展,且易于接入的权限系统.就显得至关重要了。结合以前我实现的的权限系统,今天就来和你们探讨一下我对权限系统的理解。前端
这篇文章会从权限系统业务设计,技术架构,关键代码几个方面,详细的阐述权限系统的实现。java
权限系统是一个系统的基础功能,可是做为创业公司,秉承着快比完美更重要原则,老系统的权限系统都是硬编码在代码或者写在到配置文件中的。随着业务的发展,如此简陋的权限系统就显得捉襟见肘了。开发一套新的,强大的权限系统就提上了日程。web
这里有两个重点:算法
权限系统须要支持功能权限和数据权限。spring
所谓功能权限,就是指,拥有某种角色的用户,只能看到某些功能,并使用它。实现功能权限就简化为:sql
所谓数据权限是指,数据是隔离的,用户能看到的数据,是通过控制的,用户只能看到拥有权限的某些数据。shell
好比,某个地区的 leader 能够查看并操做这个地区的全部员工负责的订单数据,可是员工就只能操做和查看本身负责的的订单数据。编程
对于数据权限,咱们须要考虑的问题就抽象为,后端
通过上面的分析,咱们能够抽象出如下几个实体:api
咱们知道,对于一某个功能来讲,它是由若干的前端元素和后端 API 组成的。
好比“合同审核” 这个功能就包括了,“查看按钮”、“审核按钮” 等前端元素。
涉及的 api 就可能包含了 contract
的 get
和 patch
两个 Restful 风格的接口。
抽象出来就是:在权限系统中若干前端元素和后端 API 组成了一个功能。
具体的关系,就是以下图:
具体每一个系统的数据权限的实现有所不一样,咱们这里实现的数据权限是依赖于公司的组织架构实现的,全部涉及到的实体以下:
这里须要说明一下,要接入数据权限,首先须要梳理数据的归属问题,数据归属于谁?或者准确的来讲,数据属于哪一个数据拥有者,这个数据拥有者属于哪一个部门。经过这个关联关系咱们就能够明确,这个数据属于哪一个部门。
对于数据的使用用户,来讲,就须要查询,这个用户能够查看某个模块的某个部门的数据。
这里须要说明的是,不一样的系统的数据权限须要具体分析,咱们系统的数据权限是创建在公司的组织架构上的。
本质就是:
具体的关系图以下:
注意,实际上用户和数据拥有者都是同一个实体 User 表示,只是为了表述方便进行了区分。
能够看出来,咱们的功能和组织架构都是典型的树形结构。
咱们最多见的场景以下
抽象之后就是查询树的某个节点,和他的全部子节点。
为了便于查询,咱们能够增长两个冗余字段,一个是 parent_id
,还有一个是 path
。
A
/ \
B C
/\ /\
D E F G
/\
H I
复制代码
对于 D 的 path 就是 (A.id).(B.id).
这要的好处的就是经过 sql
的 like
的语句就能快速的查询出某个节点的子节点。
好比要获取节点 C 的全部子节点:
Select * from user where path like (A.id).(C.id).%
复制代码
一次查询能够获取全部子节点,是一种查询友好的设计。若是须要咱们能够为 path
字段增长索引,根据索引的左值定律,这样的 like 查询是能够走索引的。提高查询效率。
咱们知道 Spirng mvc
在启动的时候会扫描被 @RequestMapping
注解标记的方法,并把数据放在 RequestMappingHandlerMapping
中。因此咱们能够这样:
@Componet
public class ApiScanSerivce{
@Autoired
private RequestMappingHandlerMapping requestMapping;
@PostConstruct
public void update(){
Map<RequestMappingInfo,HandlerMethed> handlerMethods = requestMapping.getHandlerMethods();
for(Map.Entry RequestMappinInfo,HandlerMethod) entry: handlerMethods.entrySet(){
// 处理 API 上传的相关逻辑
updateApiInfo();
}
}
}
复制代码
获取项目的全部 http 接口。这样咱们就能够遍历处理项目的接口数据。
public class ApiInfo{
private Long id;
private String uri; // api 的 uri
private String method; //请求的 method:eg: get、 post、 patch。
private String project; // 这组 api 属于哪个 web 工程。
private String signature; //方法的签名
private Intger status; // api 状态
private Intger whiteList; // 是不是白名单 api 若是是就不需过滤
}
复制代码
其中方法的签名生成的算法伪代码:
signature = className + "#" + methodName +"(" + parameterTypeList+")"
复制代码
首先咱们定义的用户权限数据以下:
@Data
@ToString
public class UserPermisson{
//用户能够看到的前端元素的列表
private List<Long> pageElementIdList;
//用户可使用的 API 列表
private List<String> apiSignatureList;
//用户不一样模块的数据权限 的 map。map 的 key 是模块名称,value 是这个可以看到数据属于那些用户的列表
private Map<String,List<Long>> dataAccessMap;
}
复制代码
对于如何使用 Spring 实现方法拦截,很天然的就像到了使用拦截器来实现。考虑到咱们这个权限的组件是一个通用组件,因此就能够写一个抽象类,暴露出getUid(HttpServletRequest requset)
用户获取使用系统的 userId
,以及 onPermission(String msg)
留给业务方本身实现,没有权限之后的动做。
public abstract class PermissonAbstractInterceptor extends HandlerInterceptorAdapter{
protected abstarct long getUid(HttpServletRequest requset);
protected abstract onPermession(String str) throws Exception;
@Override
public boolean preHandler(HttpServletRequest request,HttoServletResponse respponse,Object handler) throws Excption{
// 获取用户的 uid
long uid = getUid(request);
// 根据用户 获取用户相关的 权限对象
UserPermisson userPermission = getUserPermissonByUid(uid);
if(inandler instanceof HanderMethod){
//获取请求方的签名
String methodSignerture = getMethodSignerture(handler);
if(!userPermisson.getApiSignatureList().contains(methodSignerture)){
onPermession("该用户没有权限");
}
}
}
}
复制代码
以上的代码只是提供一个思路。不是真实的代码实现。
因此接入方就只须要继承这个抽象方法,并实现对应的方法,若是你使用的是 Springboot 的,只须要把实现的拦截器注册到拦截器里面就可使用了:
@Configuration
public class MyWebAppConfigurer extends WebMvcConfigurerAdapter {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(permissionInterceptor);
super.addInterceptors(registry);
}
}
复制代码
经过上面的代码能够看出来,功能权限的实现,基本作到了没有侵入代码。对于数据权限的实现的原则仍是尽可能少的减小代码的入侵。
咱们默认代码使用 Java 经典的 Controller、Service、Dao 三层架构。 主要使用的技术 Spring Aop、Jpa 的 filter,基本的实现思路以下图:
基本的思路以下:
经过图片咱们能够看出,咱们基本不须要对 Controller、Service、Dao 进行修改,只须要按需实现对应模块的 filter。
看到这里你可能以为"嚯~~",还有这种操做?咱们就看看代码是怎么具体实现的吧。
首先须要在 Entity 上写一个 Filter,假设咱们写的是订单模块。
@Entity
@Table(name = "order")
@Data
@ToString
@FilterDef(name = "orderOwnerFilter", parameters = {@ParamDef name= "ownerIds",type = "long"})
@Filters({@Filter name= "orderOwnerFiler", condition = "ownder in (:ownerIds)"})
public class order{
private Long id;
private Long ownerId;
//其余参数省略
}
复制代码
写个注解
@Retention(RetentinPolicy.RUNTIME)
@Taget(ElementType.METHOD)
public @interface OrderFilter{
}
复制代码
编写一个切面用于处理 Session、datePermission、和 Filter
@Component
@Aspect
public class OrderFilterAdvice{
@PersistenceContext
private EntityManager entityManager;
@Around("annotation(OrderFilter)")
pblict Object doProcess (ProceedingJoinPoint joinPonit) throws ThrowableP{
try{
//从上下文里面获取 owerId,这个 Id 在 web 中就已经存好了
List<Long> ownerIds = getListFromThreadLocal();
//获取查询中的 session
Session session = entityManager.unwrap(Session.class);
// 在 session 中加入 filter
Filter filter = unwrap.enableFilter("orderOwnerFilter");
// filter 中加入数据
filter.setParameterList("ownerIds",ownerIds)
//执行 被拦截的方法
return join.proceed();
}catch(Throwable e){
log.error();
}finally{
// 最后 disable filter
entityManager.unwrap(Session.class).disbaleFilter("orderOwnerFilter");
}
}
}
复制代码
这个拦截器,拦截被打了 @OrderFilter
的方法。
为了方便接入项目,咱们能够将涉及到的整套代码封装为一个 springboot-starter
这样使用者只须要引入对应的 starter 就可以接入权限系统。
权限系统随着业务的发展,是从能够没有逐渐变成为很是重要的模块。每每须要接入权限系统的时候,系统已经成熟的运行了一段时间了。大量的接口,负责的业务,为权限系统的接入提升了难度。同时权限系统又是看似通用,可是定制的点又很多的系统。
设计套权限系统的初衷就是,不须要大量修改代码,业务方就可方便简单的接入。 具体实现代码的时候,咱们充分利用了面向切面的编程思想。同时大量的使用了 Spring
、Hibrenate
框架的高级特性,保证的代码的灵活,以及横向扩展的能力。
看完文章若是你发现有疑问,或者更好的实现方法,欢迎留言与我讨论。