Springboot Mybatis底层权限体系架构
通常项目都会有数据权限隔离的要求,比如:只能看本部门的数据、只能看经手过的数据、只能看分管条线的数据。比较简单的处理就是封装一些处理权限的方法,业务代码里每次查询数据的时候都调用这些封装的方法。当然你也可以不封装,每次查询都写一遍权限相关的逻辑。
我这里介绍一种更底层的封装方法,对业务代码的侵入较小。
1. 业务和情况介绍
业务需求千变万化,我的项目数据权限大概分为2种情况,2种情况是或关系
- 按部门隔离。默认只能查看自己部门、兼职部门、分管部门的数据
- 按业务分类隔离。如果分管某项业务,就可以查看这项业务的所有数据
项目初期没有规划这样的权限体系,关键字段命名也没有做约定,这给改造添加了一点点难度:
- 要用的字段是部门、课程分类,原则上2个都是树形结构
- 字段名字可能不一样:比如部门,有的是dept_id,有的是org_id
- 存储方式可能不一样,有的表直接存了其中1个或2个字段,有个存了关联关系的字段
2. 整体思路
结合spring切面、MyBatis拦截器来实现
- MyBatis支持自定义拦截器,拦截器提供了修改sql的方法
- 定义一个切面,通过注解传递参数给切面用以处理字段名和存储方式的问题
拦截器要实现Mybatis的Interceptor
接口,在拦截器里获取到的信息有限,所以我借助了切面。
- 在切面里获取当前登录人的权限信息,并存入缓存
- 在拦截器读取缓存里的权限信息,拼接权限sql
最终的sql是:
前置sql where (业务sql) and (部门隔离sql or 产品隔离sql or 自定义sql)
3. 切面实现
定义注解DataSegregate,有3个参数:
- Org:部门id对应表别名和字段
- ProductType: 产品分类id对应表别名和字段
- SomeThing: 其他自定义sql
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSegregate {
String Org() default "";
String ProductType() default "";
String SomeThing() default "";
}
定义切面DataSegregateAspect
。在这里读取当前用户的权限:是否具有所有数据权限、权限模块配置的部门、权限系统配置的产品、兼职的部门,并存入缓存。 我这个项目使用的是session,所以直接存session了。各位看官如果参考我的方法,可以考虑几个优化点:
- 存session改为存redis或者其他缓存工具,更为灵活和通用
- 现在是每次查询都触发读取最新权限配置,消耗了系统算力。可以改为权限配置调整、兼职调整的时候触发
@Aspect
@Component
public class DataSegregateAspect {
@Resource private IOrgService orgService;
@Resource private IPriConfigService priConfigService;
@Resource private IRoleService roleService;
@Pointcut("@annotation(com.master.app.config.DataSegregate)")
public void DataSegregatePointCut() {}
@Before("DataSegregatePointCut()")
public void doBefore(JoinPoint point) throws Throwable {
handleDataScope(point);
}
protected void handleDataScope(final JoinPoint joinPoint) {
//获得注解
DataSegregate DataSegregate = getAnnotationByJoinPoint(joinPoint);
if (DataSegregate == null) {
return;
}
//所有权限判断。在拦截器判断所有数据权限
String allDataRoleNames = "能查看所有数据的角色";
ServiceResult<Boolean> allDataFlag = roleService.checkUserWithRoleNames(allDataRoleNames);
Map map = new HashMap();
if(allDataFlag.getData()){
//设置所有权限
map.put(SessionConst.CURRENT_USER_PRI_ALL_FLAG, true);
}
//读取当前用户权限
List<PriConfig> priConfigList = priConfigService.getListByCurUser().getData();
//配置部门
List<String> configOrgList = priConfigList.stream().filter(u -> "org".equals(u.getTargetType())).map(PriConfig::getTargetId).collect(Collectors.toList());
//兼职部门
List<String> positionOrgList = orgService.getOrgListByCurrentUser().getData().stream().map(Org::getOrgId).collect(Collectors.toList());
//配置产品
List<String> productTypeList = priConfigList.stream().filter(u -> "productType".equals(u.getTargetType())).map(PriConfig::getTargetId).collect(Collectors.toList());
map.put(SessionConst.CURRENT_USER_PRI_ORG_LIST, configOrgList);
map.put(SessionConst.CURRENT_USER_PRI_PRODUCT_TYPE_LIST, positionOrgList);
map.put(SessionConst.CURRENT_USER_POSITION_ORG_LIST, productTypeList);
UserUtil.setUserDataSegregate(map);
}
/**
* 是否存在注解,如果存在就获取
*/
private DataSegregate getAnnotationByJoinPoint(JoinPoint joinPoint) {
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod();
if (method != null) {
return method.getAnnotation(DataSegregate.class);
}
return null;
}
}
4. Mybatis拦截器实现
新建一个拦截器DataSegregateInterceptor。大致思路是把权限隔离的逻辑加入到sql里,最终的sql是:
前置sql where (业务sql) and (部门隔离sql or 产品隔离sql or 自定义sql)
@Component
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
public class DataSegregateInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
V_UserWithPosition curUser = UserUtil.getCurrentUser();
if(curUser == null){
return invocation.proceed();
}
// 拿到mybatis的一些对象
StatementHandler statementHandler = PluginUtils.realTarget(invocation.getTarget());
MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
String clazzName = mappedStatement.getId().substring(0, mappedStatement.getId().lastIndexOf("."));
String methodName = mappedStatement.getId().substring(mappedStatement.getId().lastIndexOf(".") + 1, mappedStatement.getId().length());
//判断所有数据权限
Map priConfig = UserUtil.getUserDataSegregate();
boolean allPrivilegeFlag = false;
if(priConfig.get(SessionConst.CURRENT_USER_PRI_ALL_FLAG) != null){
allPrivilegeFlag = (Boolean)priConfig.get(SessionConst.CURRENT_USER_PRI_ALL_FLAG);
}
if(allPrivilegeFlag){
return invocation.proceed();
}
//是否开启数据权限
Class<?> clazz = Class.forName(clazzName);
DataSegregate annotation = null;
//遍历方法
for (Method method : clazz.getDeclaredMethods()) {
//方法是否含有DataPermission注解,如果含有注解则将数据结果过滤
if(method.getName().equals(methodName)){
annotation = method.getAnnotation(DataSegregate.class);
}
}
if (annotation == null){
return invocation.proceed();
}
String sql = statementHandler.getBoundSql().getSql();
// 解析并返回新的SQL语句,只处理查询sql
if (mappedStatement.getSqlCommandType().toString().equals("SELECT")) {
sql = getSql(sql, annotation, priConfig);
}
// 修改sql
metaObject.setValue("delegate.boundSql.sql", sql);
return invocation.proceed();
}
/**
* 解析SQL语句,并返回新的SQL语句
*/
private String getSql(String sql, DataSegregate annotation, Map priConfig) throws Exception{
String condition = "";
List<String> configOrgList = new ArrayList<>();
List<String> positionOrgList = new ArrayList<>();
List<String> productTypeList = new ArrayList<>();
boolean allPrivilegeFlag = false;
String orgAlia = annotation.Org();
String productTypeAlias = annotation.ProductType();
String someThing = annotation.SomeThing();
if(priConfig.get(SessionConst.CURRENT_USER_PRI_ORG_LIST) != null){
configOrgList = (List<String>)priConfig.get(SessionConst.CURRENT_USER_PRI_ORG_LIST);
}
if(priConfig.get(SessionConst.CURRENT_USER_PRI_PRODUCT_TYPE_LIST) != null){
productTypeList = (List<String>)priConfig.get(SessionConst.CURRENT_USER_PRI_PRODUCT_TYPE_LIST);
}
if(priConfig.get(SessionConst.CURRENT_USER_POSITION_ORG_LIST) != null){
positionOrgList = (List<String>)priConfig.get(SessionConst.CURRENT_USER_POSITION_ORG_LIST);
}
if(priConfig.get(SessionConst.CURRENT_USER_PRI_ALL_FLAG) != null){
allPrivilegeFlag = (Boolean)priConfig.get(SessionConst.CURRENT_USER_PRI_ALL_FLAG);
}
if(!allPrivilegeFlag && (configOrgList==null || configOrgList.size() == 0)
&& (productTypeList==null || productTypeList.size() == 0)
&& (positionOrgList==null || positionOrgList.size() == 0)
){
throw new RuntimeException("您无权限查看数据!");
}
List<String> orgIdList = new ArrayList<>();
if(configOrgList != null){
orgIdList.addAll(configOrgList);
}
if(positionOrgList != null){
orgIdList.addAll(positionOrgList);
}
StringBuilder sqlParams = new StringBuilder();
orgIdList.forEach(p -> {
sqlParams.append(String.format("'%s',", p));
});
if(sqlParams.length() > 0 && !StringUtils.isEmpty(orgAlia)){
String idStr = sqlParams.delete(sqlParams.length() - 1, sqlParams.length()).toString();
condition += (String.format(" %s in ( %s ) ", orgAlia, idStr));
}
sqlParams.setLength(0);
productTypeList.forEach(p -> {
sqlParams.append(String.format("'%s',", p));
});
if(sqlParams.length() > 0 && !StringUtils.isEmpty(productTypeAlias)){
String idStr = sqlParams.delete(sqlParams.length() - 1, sqlParams.length()).toString();
if(condition.length() == 0)
condition = " and ";
else
condition += " or ";
condition += (String.format(" %s in ( %s ) ", productTypeAlias, idStr));
}
//todo 添加自定义sql。自定义sql与校区、分类是OR关系
if (!StringUtils.isEmpty(someThing) && sqlParams.length() > 0) {
String idStr = sqlParams.delete(sqlParams.length() - 1, sqlParams.length()).toString();
if(condition.length() > 0) condition += " or ";
condition += " " + someThing.replaceAll("#org_id", idStr);
}
if (StringUtils.isEmpty(condition)) {
throw new RuntimeException("您无权限查看数据!");
}
condition = "(" + condition + ")";
Select select = (Select) CCJSqlParserUtil.parse(sql);
PlainSelect plainSelect = (PlainSelect) select.getSelectBody();
//取得原SQL的where条件
final Expression expression = plainSelect.getWhere();
//增加新的where条件
final Expression envCondition = CCJSqlParserUtil.parseCondExpression(condition);
if (expression == null) {
plainSelect.setWhere(envCondition);
} else {
AndExpression andExpression = new AndExpression(expression, envCondition);
plainSelect.setWhere(andExpression);
}
return plainSelect.toString();
}
@Override
public Object plugin(Object target) {
if (target instanceof StatementHandler) {
return Plugin.wrap(target, this);
}
return target;
}
}
使用
在需要数据隔离的mapper层方法上添加注解即可
@DataSegregate(Org = "o.org_id", ProductType = "c.product_type")
List<Map<String, Object>> findSomeThing(Map<String, Object> map);
其他
有不足之处或者其他思路,欢迎交流