前言
传统管理系统权限通常分为功能型权限和数据权限。
**功能型权限**:通常基于RBAC模式进行设计。RBAC(Role-Based Access Control)基于角色的访问控制。
相当成熟的权限模型。三个要素:用户、角色、权限。用户与角色是多对多关系,角色与权限是多对多关系。
关键元素:
用户(员工):成功认证并登录系统的操作员(主体:who);
权限:访问资源的许可(how);
角色:权限的集合体;
业务系统:每个独立业务系统,包括各业务系统中的菜单、操作按钮等(what)
通过配置用户的角色,角色关联权限(即菜单、操作按钮等资源),实现用户对资源的访问。
**数据权限:**是指对系统用户进行数据资源可见性的控制,通俗的解释就是:符合某条件的用户只能看到该条件
下对应的数据资源。简单的数据权限就是:用户只能看到自己的数据。
如:领导需要看到所有下属员工的客户数据,员工只能看自己的客户数据;
角色A能看到全国的产品数据,角色B只能看到上海的产品数据;
功能权限和数据权限是相互独立.
ACL:访问控制列表( ACL)是附加到对象的权限列表,ACL指定哪些身份被授予对给定对象的哪些操作。在单个域对象上定义特定用户/角色的权限——而不是在典型的每个操作级别上全面定义权限。Spring Security访问控制列表 是一个支持域对象安全的Spring组件。
Spring Security ACL 核心概念和组件
Spring Security ACL 简介
Spring Security ACL 简介
一、数据权限
本文章只分析 基于部门资源下数据权限的设计与实现。设计与代码为:若依ruoyi开源架构
三、源代码下载
若依源代码
四、数据库权限设计图
1.若依ruoyi数据表间的关联没有创建外键,表与表间的关联性通过业务代码维护。
2.通过图上可知,数据权限分 自定义角色-部门权限、部门数据权限两种:
1) 自定义角色-部门权限:用户-----系统角色部门(sys-role-dept);
2) 部门数据权限:用户-----系统部门(sys-dept);
#sys_role
CREATE TABLE `sys_role` (
`role_id` bigint NOT NULL AUTO_INCREMENT COMMENT '角色ID',
`role_name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '角色名称',
`data_scope` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT '1' COMMENT '数据范围(1:全部数据权限 2:自定数据权限 3:本部门数据权限 4:本部门及以下数据权限 5:本人数据权限)',
......
PRIMARY KEY (`role_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=137 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='角色信息表';
#sys_dept
CREATE TABLE `sys_dept` (
`dept_id` bigint NOT NULL AUTO_INCREMENT COMMENT '部门id',
`parent_id` bigint DEFAULT '0' COMMENT '父部门id',
`ancestors` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '祖级列表',
`dept_name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '部门名称',
........
PRIMARY KEY (`dept_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=232 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='部门表';
data_scope
:数据范围 1:全部数据权限[即按菜单权限进行资源的过滤]
2:自定数据权限 [可选择指定特殊或其它多部门,进行跨部门数据权限控制,即:
自定义角色-部门权限]
3:本部门数据权限 [用于 方式二:部门数据权限 ]
4:本部门及以下数据权限[sys_dept表ancestors字段存储部门上、下级关联,用于
方式二:部门数据权限]
5:本人数据权限
五、数据权限前台界面
实现:自定义角色-部门权限:所选多个部门与 某个角色关联,存储于角色-部门中间表sys-role-dept,通过用户的角色查询所有的部门,再进行用户部门的数据过滤。
实现:不涉及 角色-部门中间表sys-role-dept,通过用户、部门以及部门表ancestors字段(存储部门上、下级关系)进行过滤
六、数据权限服务端
6.1 aop 拦截“数据范围”
/**
* 1,定义 数据权限过滤注解
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataScope
{
/**
* 部门表的别名
*/
public String deptAlias() default "";
/**
* 用户表的别名
*/
public String userAlias() default "";
}
/**
* 2.数据过滤处理类DataScopeAspect:aop切面方式
*/
@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";
/**
* 对带有注解DataScope的类方法在执行前进行拦截
*/
@Before("@annotation(controllerDataScope)")
public void doBefore(JoinPoint point, DataScope controllerDataScope) throws Throwable
{
clearDataScope(point);
handleDataScope(point, controllerDataScope);
}
/**
* 拦截时执行的数据过滤处理
*/
protected void handleDataScope(final JoinPoint joinPoint, DataScope controllerDataScope)
{
// 获取当前的用户
LoginUser loginUser = SecurityUtils.getLoginUser();
if (StringUtils.isNotNull(loginUser))
{
SysUser currentUser = loginUser.getUser();
// 如果是超级管理员,则不过滤数据
if (StringUtils.isNotNull(currentUser) && !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))
{
/*ancestors:存储sys_dept祖先/上级 部门列表dept_id,如:ancestors=0,100,101*/
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)
{
//向 每个资源对象(如SysUser:必须继承BaseEntity基础实体类)注入请求参数:private Map<String, Object> params;
BaseEntity baseEntity = (BaseEntity) params;
baseEntity.getParams().put(DATA_SCOPE, " AND (" + sqlString.substring(4) + ")");
}
}
}
......
}
每个资源对象,如SysUser必须继承BaseEntity基础实体类,如:
//BaseEntity
/**
* Entity基类
*/
public class BaseEntity implements Serializable
{
private static final long serialVersionUID = 1L;
.......
/** 请求参数 */
private Map<String, Object> params;
}
//SysUser
/**
* 用户对象 sys_user:继承BaseEntity基础实体类
*/
public class SysUser extends BaseEntity
.......
}
6.2 数据实现层ServiceImpl 埋点
/**
* 根据条件分页查询用户列表
* @DataScope(deptAlias = "d", userAlias = "u") 数据范围 DataScope切点 进行 埋点,被 数据过滤处理类DataScopeAspect 拦截
* @param user 用户信息
* @return 用户信息集合信息
*/
@Override
@DataScope(deptAlias = "d", userAlias = "u")
public List<SysUser> selectUserList(SysUser user)
{
return userMapper.selectUserList(user);
}
mybitais 数据层SysUserMapper.xml查询sql:
<select id="selectUserList" parameterType="SysUser" resultMap="SysUserResult">
select u.user_id, u.dept_id, u.nick_name, u.user_name, u.email, u.avatar, u.phonenumber, u.password, u.sex, 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="userName != null and userName != ''">
AND u.user_name like concat('%', #{userName}, '%')
</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>
说明:数据范围过滤 通过BaseEntity类 ${params.dataScope}
七、总结
7.1设计思路
1.在需要数据范围类实现方法中埋入切点:@DataScope(deptAlias = “d”, userAlias = “u”)
2.数据过滤处理类DataScopeAspect 切面方式拦截并根据用户数据范围方式
[1:全部数据权限 2:自定数据权限 3:本部门数据权限 4:本部门及以下数
据权限 5:本人数据权限] 进行分类处理
3.生成预查询sql片断,并回写到资源对象请求参数Map<String, Object> params中
4.执行查询时,mybitais 资源数据层XxxxxMapper.xml预定义对象请求参数params
7.2 缺陷
mybitais 资源数据层XxxxxMapper.xml sql需要并联部门(sys-dept)表