深入分析若依数据权限@datascope (注解+AOP+动态sql拼接) 循序渐进,附分析过程
Posted 金发罗婕
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入分析若依数据权限@datascope (注解+AOP+动态sql拼接) 循序渐进,附分析过程相关的知识,希望对你有一定的参考价值。
笔者最近在努力的分析开源项目若依框架,今天看到了若依对数据权限进行控制的部分,自定义注解+AOP+动态SQL的注入,看的我是眼花缭乱,然后我又认真的复盘了一遍整个的实现过程,不由得感叹一句,若依YYDS~~
简单猜测
除了我们平时都知道的 路由权限(即对接口的访问权限外),在日常生产开发中,我们还应该有对数据的访问权限。
在若依这个框架中,通过角色中的数据范围这个属性来对数据权限进行控制。
对应实体类:
深入分析
一个用户肯定是 有一种角色的,也肯定是隶属于一个部门的。
这里咱们就以用户在查询用户时,即 selectUserList时所做的数据权限为例进行分析。
若依在进行数据权限的访问时,持久层(Mapper层)中对数据进行处理,根据用户角色的权限对数据进行过滤。
<select id="selectUserList" parameterType="SysUser" resultMap="SysUserResult">
select u.user_id, u.dept_id, u.login_name, u.user_name, u.user_type, u.email, u.avatar, u.phonenumber, u.password, u.sex, u.salt, u.status, u.del_flag, u.login_ip, u.login_date, u.create_by, u.create_time, u.remark, d.dept_name, d.leader from sys_user u
left join sys_dept d on u.dept_id = d.dept_id
where u.del_flag = '0'
<if test="userId != null and userId != 0">
AND u.user_id = #userId
</if>
<if test="loginName != null and loginName != ''">
AND u.login_name like concat('%', #loginName, '%')
</if>
<if test="status != null and status != ''">
AND u.status = #status
</if>
<if test="phonenumber != null and phonenumber != ''">
AND u.phonenumber like concat('%', #phonenumber, '%')
</if>
<if test="params.beginTime != null and params.beginTime != ''"><!-- 开始时间检索 -->
AND date_format(u.create_time,'%y%m%d') >= date_format(#params.beginTime,'%y%m%d')
</if>
<if test="params.endTime != null and params.endTime != ''"><!-- 结束时间检索 -->
AND date_format(u.create_time,'%y%m%d') <= date_format(#params.endTime,'%y%m%d')
</if>
<if test="deptId != null and deptId != 0">
AND (u.dept_id = #deptId OR u.dept_id IN ( SELECT t.dept_id FROM sys_dept t WHERE FIND_IN_SET (#deptId,ancestors) ))
</if>
<!-- 数据范围过滤 -->
$params.dataScope
</select>
我们可以看到倒数第二行$params.dataScope就是对数据范围进行过滤
其中,params指的是parameterType="SysUser"传来的参数 SysUse的一个属性,然后这个属性的dataScope属性。
思考:既然params.dataScope通过占位符嵌入在这里,那么他肯定是一个sql语句。我们返回到sysUser实体类中,发现sysUser中并没有params 这个属性。
这里我们可以看到SysUser继承了BaseEntity,果然我们在BaseEntity这个类中发现了 params 这个属性
public class BaseEntity implements Serializable
private static final long serialVersionUID = 1L;
/** 搜索值 */
private String searchValue;
/** 创建者 */
private String createBy;
/** 创建时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
/** 更新者 */
private String updateBy;
/** 更新时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date updateTime;
/** 备注 */
private String remark;
/** 请求参数 */
private Map<String, Object> params;
public String getSearchValue()
return searchValue;
public void setSearchValue(String searchValue)
this.searchValue = searchValue;
public String getCreateBy()
return createBy;
public void setCreateBy(String createBy)
this.createBy = createBy;
public Date getCreateTime()
return createTime;
public void setCreateTime(Date createTime)
this.createTime = createTime;
public String getUpdateBy()
return updateBy;
public void setUpdateBy(String updateBy)
this.updateBy = updateBy;
public Date getUpdateTime()
return updateTime;
public void setUpdateTime(Date updateTime)
this.updateTime = updateTime;
public String getRemark()
return remark;
public void setRemark(String remark)
this.remark = remark;
public Map<String, Object> getParams()
if (params == null)
params = new HashMap<>();
return params;
public void setParams(Map<String, Object> params)
this.params = params;
到这里 我们已经知道有 params这个属性的存在,并且能够肯定 params.dataScope 这是一个sql语句。 所以我们下一步就是要确定这个sql语句是什么时候注入的,怎么注入的?
高阶玩法
我们能够发现若依自定义了一个注解 @DataScope
/**
* 数据权限过滤注解
*
* @author ruoyi
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataScope
/**
* 部门表的别名
*/
public String deptAlias() default "";
/**
* 用户表的别名
*/
public String userAlias() default "";
我们继续探究这个注解是怎么执行的
我们可以发现在 SysUserServiceImpl Service业务层实现类中发现 @DataScope 的使用
@Override
@DataScope(deptAlias = "d", userAlias = "u")
public List<SysUser> selectUserList(SysUser user)
return userMapper.selectUserList(user);
我们现在来观察DataScopeAspect 这个切面类,具体实现步骤我都在旁边进行注解了
/**
* 数据过滤处理
*
* @author ruoyi
*/
@Aspect
@Component
public class DataScopeAspect
/**
* 全部数据权限
*/
public static final String DATA_SCOPE_ALL = "1";
/**
* 自定数据权限
*/
public static final String DATA_SCOPE_CUSTOM = "2";
/**
* 部门数据权限
*/
public static final String DATA_SCOPE_DEPT = "3";
/**
* 部门及以下数据权限
*/
public static final String DATA_SCOPE_DEPT_AND_CHILD = "4";
/**
* 仅本人数据权限
*/
public static final String DATA_SCOPE_SELF = "5";
/**
* 数据权限过滤关键字
*/
public static final String DATA_SCOPE = "dataScope";
@Before("@annotation(controllerDataScope)") // 这个controllerDataScope是一个形参
public void doBefore(JoinPoint point, DataScope controllerDataScope) throws Throwable
clearDataScope(point);
handleDataScope(point, controllerDataScope);
protected void handleDataScope(final JoinPoint joinPoint, DataScope controllerDataScope)
// 获取当前的用户
SysUser currentUser = ShiroUtils.getSysUser();
if (currentUser != null)
// 如果是超级管理员,则不过滤数据
if (!currentUser.isAdmin())
dataScopeFilter(joinPoint, currentUser, controllerDataScope.deptAlias(),
controllerDataScope.userAlias());
/**
* 数据范围过滤
*
* @param joinPoint 切点
* @param user 用户
* @param deptAlias 部门别名
* @param userAlias 用户别名
*/
public static void dataScopeFilter(JoinPoint joinPoint, SysUser user, String deptAlias, String userAlias)
StringBuilder sqlString = new StringBuilder();
for (SysRole role : user.getRoles()) //获取当前用户的角色
String dataScope = role.getDataScope(); //获取角色的数据权限
// 若这个角色的数据权限是 全部数据权限
if (DATA_SCOPE_ALL.equals(dataScope))
sqlString = new StringBuilder();
break;
//自定义数据权限
else if (DATA_SCOPE_CUSTOM.equals(dataScope))
sqlString.append(StringUtils.format(
" OR .dept_id IN ( SELECT dept_id FROM sys_role_dept WHERE role_id = ) ", deptAlias,
role.getRoleId()));
//本部门数据权限
else if (DATA_SCOPE_DEPT.equals(dataScope))
sqlString.append(StringUtils.format(" OR .dept_id = ", deptAlias, user.getDeptId()));
//本部门及以下数据权限
else if (DATA_SCOPE_DEPT_AND_CHILD.equals(dataScope))
sqlString.append(StringUtils.format(
" OR .dept_id IN ( SELECT dept_id FROM sys_dept WHERE dept_id = or find_in_set( , ancestors ) )",
deptAlias, user.getDeptId(), user.getDeptId()));
//仅本人数据权限
else if (DATA_SCOPE_SELF.equals(dataScope))
if (StringUtils.isNotBlank(userAlias))
sqlString.append(StringUtils.format(" OR .user_id = ", userAlias, user.getUserId()));
else
// 数据权限为仅本人且没有userAlias别名不查询任何数据
sqlString.append(" OR 1=0 ");
if (StringUtils.isNotBlank(sqlString.toString()))
Object params = joinPoint.getArgs()[0];
if (StringUtils.isNotNull(params) && params instanceof BaseEntity)
BaseEntity baseEntity = (BaseEntity) params;
baseEntity.getParams().put(DATA_SCOPE, " AND (" + sqlString.substring(4) + ")");
//将完成好的sql语句放在实体类 params的 dataScope属性中 这个属性是一个Map
/**
* 拼接权限sql前先清空params.dataScope参数防止注入
*/
private void clearDataScope(final JoinPoint joinPoint)
Object params = joinPoint.getArgs()[0];
if (StringUtils.isNotNull(params) && params instanceof BaseEntity)
BaseEntity baseEntity = (BaseEntity) params;
baseEntity.getParams().put(DATA_SCOPE, "");
!!!!完结撒花!!!
如果你对这个过程看懂了的话,相信你也能够数据库中 sys_role_dept这个表的含义了吧,笔者当时也是不明白这个到底是个什么意思。其实这张表就是存放那些 自定义数据权限的角色和它对应的部门数据权限。
以上是关于深入分析若依数据权限@datascope (注解+AOP+动态sql拼接) 循序渐进,附分析过程的主要内容,如果未能解决你的问题,请参考以下文章
SaToken使用SpringBoot整合SaToken关于数据权限
SaToken使用SpringBoot整合SaToken关于数据权限