不同用户在某页面看到数据不一致,实现每个用户之间数据隔离的效果。
如以下场景:
● 页面期望展示当前登录人所在部门的数据。
● 页面期望展示当前登录人所在部门及下级部门的数据。
● 页面期望展示当前登录人创建的数据。
接下来我们实现一个简单的数据权限控制,规则只包括自定义sql,目的是在需要的时候将自定义sql拼接到sql中,并将变量替换成对应的值。
1、首先确定用户-角色-菜单-数据权限的关系
菜单有多个数据权限
角色可以绑定多个菜单的,绑定菜单时可以绑定数据权限
用户与角色绑定。
2、定义注解,在方法上使用注解标识当前接口对应的菜单,为了查询数据权限规则,同时该注解可以作为切面的切入点。
/**
* 数据权限注解
* @Author taoyan
* @Date 2019年4月11日
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE,ElementType.METHOD})
@Documented
public @interface PermissionData {
/**
* 配置菜单的组件路径,用于数据权限
*/
String permissionId() default "";
}
3、定义数据权限规则信息对象
@Data
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = true)
@ApiModel(value = "菜单权限规则表对象", description = "菜单权限规则表")
public class SysPermissionDataRule extends BaseEntity {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "id", required = true)
@TableId(value = "id", type = IdType.ASSIGN_ID)
private Long id;
@ApiModelProperty(value = "对应的菜单id", required = true)
@NotNull(message = "对应的菜单id不能为空")
private Long permissionId;
@ApiModelProperty(value = "规则名称")
private String ruleName;
@ApiModelProperty(value = "规则值")
private String ruleValue;
@ApiModelProperty(value = "状态;1:有效,0:无效")
private String status;
}
4、定义切面,将用户信息和数据权限规则缓存再request中。
切面:
package com.sinosoft.springbootplus.datapermission.aspect;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import com.sinosoft.springbootplus.datapermission.aspect.annotation.PermissionData;
import com.sinosoft.springbootplus.core.context.RequestContext;
import com.sinosoft.springbootplus.datapermission.handler.PermissionDataDepInfoHandler;
import com.sinosoft.springbootplus.datapermission.handler.PermissionDataInfoHandler;
import com.sinosoft.springbootplus.system.domain.entity.SysPermissionDataRule;
import com.sinosoft.springbootplus.system.domain.service.SysPermissionDataRuleDomain;
import com.sinosoft.springbootplus.system.util.DataAutorUtils;
import com.sinosoft.springbootplus.util.HttpServletRequestUtil;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Map;
/**
* 数据权限切面处理类
* 当被请求的方法有注解PermissionData时,会在往当前request中写入数据权限信息
* @Date 2019年4月10日
* @Version: 1.0
* @author: jeecg-boot
*/
@Aspect
@Component
@Slf4j
public class PermissionDataAspect {
@Pointcut("@annotation(com.sinosoft.springbootplus.datapermission.aspect.annotation.PermissionData)")
public void pointCut() {
}
@Around("pointCut()")
public Object arround(ProceedingJoinPoint point) throws Throwable{
HttpServletRequest request = HttpServletRequestUtil.getRequest();
//将用户信息放在request请求中
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
PermissionData pd = method.getAnnotation(PermissionData.class);
String permissionId = pd.permissionId();
//根据userId和菜单id获取绑定的角色对应的数据权限
List<SysPermissionDataRule> dataRules = sysPermissionDataRuleDomain.getPermissionDataRulesByCompentAndUserId(permissionId,loginUserId);
if(dataRules != null && dataRules.size()>0){
//将权限信息暂存在request中
DataAutorUtils.installDataSearchConditon(request,dataRules);
}
//将用户信息存储在request中
DataPermissionContext dataPermissionContext = new DataPermissionContext();
//从handler中获取用户部门信息
if(ObjectUtil.isNotEmpty(permissionDataDepInfoHandler)){
Map<String, Object> userInfo = permissionDataDepInfoHandler.getUserInfo(request, loginUserId);
dataPermissionContext.putAll(userInfo);
}
//从handler中获取用户其他信息
if (CollUtil.isNotEmpty(permissionDataInfoHandlers)) {
for (PermissionDataInfoHandler permissionDataInfoHandler:permissionDataInfoHandlers) {
Map<String, Object> userInfo = permissionDataInfoHandler.getUserInfo(request, loginUserId);
if (null != userInfo) {
dataPermissionContext.putAll(userInfo);
}
}
}
DataAutorUtils.installUserInfo(dataPermissionContext);
return point.proceed();
}
}
切面中往request中放和取用户信息、数据权限的工具类
public class DataAutorUtils {
public static final String MENU_DATA_AUTHOR_RULES = "MENU_DATA_AUTHOR_RULES";
public static final String SYS_USER_INFO = "SYS_USER_INFO";
/**
* 往链接请求里面,传入数据查询条件
*/
public static synchronized void installDataSearchConditon(HttpServletRequest request, List<SysPermissionDataRule> dataRules) {
request.setAttribute(MENU_DATA_AUTHOR_RULES, dataRules);
}
/**
* 获取请求对应的数据权限规则
*/
@SuppressWarnings("unchecked")
public static synchronized List<SysPermissionDataRule> loadDataSearchConditon() {
return (List<SysPermissionDataRule>) HttpServletRequestUtil.getRequest().getAttribute(MENU_DATA_AUTHOR_RULES);
}
/**
* 将用户信息存到request
*/
public static synchronized void installUserInfo(DataPermissionContext dataPermissionContext) {
HttpServletRequestUtil.getRequest().setAttribute(SYS_USER_INFO, dataPermissionContext);
}
/**
* 从request获取用户信息
*/
public static synchronized DataPermissionContext loadUserInfo() {
return (DataPermissionContext) HttpServletRequestUtil.getRequest().getAttribute(SYS_USER_INFO);
}
}
5、在需要拼数据权限的接口上增加注解,并初始化queryWrapper,将request中的参数传进去,包括了该菜单的数据权限和用户信息
4、初始化queryWrapper将数据权限规则拼接到sql中,拼接时会将用户信息map中的信息替换成自定义sql #{ }中的值,因此放用户信息时map中的key与自定义sql中#{ }的值要对应。
/**
* 查询生成器
*/
@Slf4j
public class QueryGenerator {
public static final String SQL_RULES_COLUMN = "SQL_RULES_COLUMN";
/**
* 获取查询条件构造器QueryWrapper实例 通用查询条件已被封装完成
* @param searchObj 查询实体
* @param parameterMap request.getParameterMap()
* @return QueryWrapper实例
*/
public static <T> QueryWrapper<T> initQueryWrapper(T searchObj, Map<String, String[]> parameterMap){
long start = System.currentTimeMillis();
QueryWrapper<T> queryWrapper = new QueryWrapper<T>();
installMplus(queryWrapper, searchObj, parameterMap);
log.debug("---查询条件构造器初始化完成,耗时:"+(System.currentTimeMillis()-start)+"毫秒----");
return queryWrapper;
}
/**
* 组装Mybatis Plus 查询条件
* 使用此方法 需要有如下几点注意:
*
1.使用QueryWrapper 而非LambdaQueryWrapper;
*
2.实例化QueryWrapper时不可将实体传入参数
*
错误示例:如QueryWrapper queryWrapper = new QueryWrapper(jeecgDemo);
*
正确示例:QueryWrapper queryWrapper = new QueryWrapper();
*
3.也可以不使用这个方法直接调用 {@link #initQueryWrapper}直接获取实例
*/
private static void installMplus(QueryWrapper<?> queryWrapper,Object searchObj,Map<String, String[]> parameterMap) {
Map<String,SysPermissionDataRule> ruleMap = getRuleMap();
//权限规则自定义SQL表达式
for (String c : ruleMap.keySet()) {
if(c.startsWith(SQL_RULES_COLUMN)){
queryWrapper.and(i ->i.apply(getSqlRuleValue(ruleMap.get(c).getRuleValue())));
}
}
}
/**
* 获取请求对应的数据权限规则
*/
public static Map<String, SysPermissionDataRule> getRuleMap() {
Map<String, SysPermissionDataRule> ruleMap = new HashMap<>(5);
List<SysPermissionDataRule> list = DataAutorUtils.loadDataSearchConditon();
if(list != null&&list.size()>0){
if(list.get(0)==null){
return ruleMap;
}
for (SysPermissionDataRule rule : list) {
String column = SQL_RULES_COLUMN+rule.getId();
ruleMap.put(column, rule);
}
}
return ruleMap;
}
public static String getSqlRuleValue(String sqlRule){
try {
Set<String> varParams = getSqlRuleParams(sqlRule);
for(String var:varParams){
String tempValue = converRuleValue(var);
sqlRule = sqlRule.replace("#{"+var+"}",tempValue);
}
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return sqlRule;
}
/**
* 获取sql中的#{key} 这个key组成的set
*/
public static Set<String> getSqlRuleParams(String sql) {
if(StringUtils.isEmpty(sql)){
return null;
}
Set<String> varParams = new HashSet<String>();
String regex = "\\#\\{\\w+\\}";
Pattern p = Pattern.compile(regex);
Matcher m = p.matcher(sql);
while(m.find()){
String var = m.group();
varParams.add(var.substring(var.indexOf("{")+1,var.indexOf("}")));
}
return varParams;
}
public static String converRuleValue(String ruleValue) {
String value = getUserSystemData(ruleValue);
return value!= null ? value : ruleValue;
}
/**
* 从当前用户中获取变量
*/
public static String getUserSystemData(String key) {
DataPermissionContext dataPermissionContext = DataAutorUtils.loadUserInfo();
Object o = dataPermissionContext.get(key);
if(o instanceof String){
return dataPermissionContext.get(key).toString();
}
if(o instanceof List){
List<String> result = new ArrayList<>();
for (Object item : (List<?>) o){
result.add((String) item);
}
return "(" + String.join(",", result) + ")";
}
return null;
}
}