数据隔离

在实际开发中,需要设置用户只能查看哪些部门的数据,这种情况一般称为数据权限。
例如对于销售,财务的数据,它们是非常敏感的,因此要求对数据权限进行控制, 对于基于集团性的应用系统而言,就更多需要控制好各自公司的数据了。如设置只能看本公司、或者本部门的数据,对于特殊的领导,可能需要跨部门的数据, 因此程序不能硬编码那个领导该访问哪些数据,需要进行后台的权限和数据权限的控制。

1.RBAC模型

Role-Based Access Control,中文意思是:基于角色(Role)的访问控制。这是一种广泛应用于计算机系统和网络安全领域的访问控制模型。

简单来说,就是通过将权限分配给➡角色,再将角色分配给➡用户,来实现对系统资源的访问控制。一个用户拥有若干角色,每一个角色拥有若干权限。这样,就构造成“用户-角色-权限”的授权模型。在这种模型中,用户与角色之间,角色与权限之间,一般者是多对多的关系。具体而言,RBAC模型定义了以下几个核心概念:

  • 角色(Role):角色是指在系统中具有一组相关权限的抽象概念,代表了用户在特定上下文中的身份或职能,例如管理员、普通用户等。
  • 权限(Permission):权限是指对系统资源进行操作的许可,如读取、写入、修改等。权限可以被分配给角色。
  • 用户(User):用户是指系统的实际使用者,每个用户可以被分配一个或多个角色。
  • 分配(Assignment):分配是指将角色与用户关联起来,以赋予用户相应的权限。

RBAC 认为授权实际上是Who 、What 、How 三元组之间的关系,也就是Who 对What 进行How 的操作,也就是“主体”对“客体”的操作。

Who:是权限的拥有者或主体(如:User,Role)。

What:是操作或对象(operation,object)。

How:具体的权限(Privilege,正向授权与负向授权)。

通过RBAC模型,可以实现灵活且易于管理的访问控制策略。管理员可以通过分配和调整角色,来管理用户的权限。这种角色层次结构可以帮助简化权限管理,并确保用户只有所需的权限。

RBAC模型广泛应用于系统安全、数据库管理、网络管理等领域,它提供了一种可扩展、可管理的访问控制机制,有助于保护系统资源免受未经授权的访问和潜在的安全威胁

2.数据库设计

2.1 用户表

2.2 角色表

存在四种数据权限,一般admin使用的是1全部数据权限,后端校验不拼sql直接查询

使用第二种自定义权限需要一张中间表,依靠部门id实现隔离

2.3 用户角色关联表

2.4 菜单表

菜单表保存了目录,菜单项,按钮权限三种数据,依靠menu_type区分

2.5 角色菜单关联表

本表作用存储角色的菜单项路由,动态展示给前端

2.6 部门表

部门表是给数据权限2和3准备的,如果是2自定义数据权限,还需要一张中间表

2.7 角色部门关联表

自定义数据权限中间表

3. 后端实现

若要在自己的业务表中使用数据隔离,需要加上dept_iduser_id 字段

3.1 权限注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 数据权限过滤注解
*
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataScope
{
/**
* 部门表的别名
*/
public String deptAlias() default "";

/**
* 用户表的别名
*/
public String userAlias() default "";

/**
* 权限字符(用于多个角色匹配符合要求的权限)默认根据权限注解@ss获取,多个权限用逗号分隔开来
*/
public String permission() default "";
}

3.2 切面实现

五种数据权限

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* 全部数据权限
*/
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";

前置通知

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/**
* 拼接权限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, ""); //清空data_scope
}
}

@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())
{
//使用数据隔离的方法,控制器必须带上权限校验
String permission = StringUtils.defaultIfEmpty(controllerDataScope.permission(), PermissionContextHolder.getContext());
dataScopeFilter(joinPoint, currentUser, controllerDataScope.deptAlias(),
controllerDataScope.userAlias(), permission);
}
}
}

数据范围过滤,sql拼接

注意:使用数据过滤的接口方法必须带有权限

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
/**
* 数据范围过滤
*
* @param joinPoint 切点
* @param user 用户
* @param deptAlias 部门别名
* @param userAlias 用户别名
* @param permission 权限字符
*/
public static void dataScopeFilter(JoinPoint joinPoint, SysUser user, String deptAlias, String userAlias, String permission)
{
StringBuilder sqlString = new StringBuilder();
List<String> conditions = new ArrayList<String>();
List<String> scopeCustomIds = new ArrayList<String>(); //自定义数据权限,角色id集合
user.getRoles().forEach(role -> {
if (DATA_SCOPE_CUSTOM.equals(role.getDataScope()) && StringUtils.containsAny(role.getPermissions(), Convert.toStrArray(permission)))
{
scopeCustomIds.add(Convert.toStr(role.getRoleId())); //添加用户角色的数据权限为自定义的角色id
}
});

for (SysRole role : user.getRoles())
{
String dataScope = role.getDataScope();
if (conditions.contains(dataScope))
{
continue;
}
if (!StringUtils.containsAny(role.getPermissions(), Convert.toStrArray(permission))) //框架代码使用数据隔离必须要权限字段
{
continue;
}
if (DATA_SCOPE_ALL.equals(dataScope)) //全部数据权限
{
sqlString = new StringBuilder();
conditions.add(dataScope);
break;
}
else if (DATA_SCOPE_CUSTOM.equals(dataScope)) //自定义数据权限
{
if (scopeCustomIds.size() > 1)
{
// 多个自定数据权限使用in查询,避免多次拼接。
sqlString.append(StringUtils.format(" OR {}.dept_id IN ( SELECT dept_id FROM sys_role_dept WHERE role_id in ({}) ) ", deptAlias, String.join(",", scopeCustomIds)));
}
else
{
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(StringUtils.format(" OR {}.dept_id = 0 ", deptAlias));
}
}
conditions.add(dataScope);
}

// 角色都不包含传递过来的权限字符,这个时候sqlString也会为空,所以要限制一下,不查询任何数据
if (StringUtils.isEmpty(conditions))
{
sqlString.append(StringUtils.format(" OR {}.dept_id = 0 ", deptAlias));
}

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) + ")");
}
}
}

权限方法

@PreAuthorize注解用于配置接口要求用户拥有某些权限才可访问,它拥有如下方法

方法 参数 描述
hasPermi String 验证用户是否具备某权限
lacksPermi String 验证用户是否不具备某权限,与 hasPermi逻辑相反
hasAnyPermi String 验证用户是否具有以下任意一个权限
hasRole String 判断用户是否拥有某个角色
lacksRole String 验证用户是否不具备某角色,与 isRole逻辑相反
hasAnyRoles String 验证用户是否具有以下任意一个角色,多个逗号分隔

1. 使用示例

其中@ss代表的是PermissionService (opens new window)服务,对每个接口拦截并调用PermissionService的对应方法判断接口调用者的权限。

  1. 数据权限示例。
1
2
3
4
5
6
7
8
// 符合system:user:list权限要求
@PreAuthorize("@ss.hasPermi('system:user:list')")

// 不符合system:user:list权限要求
@PreAuthorize("@ss.lacksPermi('system:user:list')")

// 符合system:user:add或system:user:edit权限要求即可
@PreAuthorize("@ss.hasAnyPermi('system:user:add,system:user:edit')")

编程式判断是否有资源权限

1
2
3
4
if (SecurityUtils.hasPermi("sys:user:edit"))
{
System.out.println("当前用户有编辑用户权限");
}
  1. 角色权限示例。
1
2
3
4
5
6
7
8
// 属于user角色
@PreAuthorize("@ss.hasRole('user')")

// 不属于user角色
@PreAuthorize("@ss.lacksRole('user')")

// 属于user或者admin之一
@PreAuthorize("@ss.hasAnyRoles('user,admin')")

编程式判断是否有角色权限

1
2
3
4
if (SecurityUtils.hasRole("admin"))
{
System.out.println("当前用户有admin角色权限");
}

超级管理员拥有所有权限,不受权限约束。

2. 后端实现

使用@ss可以访问到自定义的service ss代表service的名字

1
2
@Service("ss")
public class PermissionService

权限校验方法

RequestContextHolder 是 Spring 框架中的一个类,用于在当前线程中存储和检索 RequestAttributes 对象。它主要用于在应用程序的任何地方访问当前的 HTTP 请求和会话对象,而不需要将这些对象作为方法参数传递。

RequestContextHolder 提供了一些静态方法,可以帮助开发者方便地访问当前请求的上下文信息:

  1. 获取当前请求的 RequestAttributes
    • RequestContextHolder.getRequestAttributes(): 返回当前线程的 RequestAttributes,如果没有可用的请求上下文,则返回 null
  2. 设置当前线程的 RequestAttributes
    • RequestContextHolder.setRequestAttributes(RequestAttributes attributes): 设置当前线程的 RequestAttributes
    • RequestContextHolder.setRequestAttributes(RequestAttributes attributes, boolean inheritable): 设置当前线程的 RequestAttributes,第二个参数指定是否应使这些属性可继承(即在子线程中是否可用)。
  3. 清除当前线程的 RequestAttributes
    • RequestContextHolder.resetRequestAttributes(): 清除当前线程的 RequestAttributes,将其设置为 null
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 权限信息
*
* @author ruoyi
*/
public class PermissionContextHolder
{
private static final String PERMISSION_CONTEXT_ATTRIBUTES = "PERMISSION_CONTEXT";

public static void setContext(String permission)
{
RequestContextHolder.currentRequestAttributes().setAttribute(PERMISSION_CONTEXT_ATTRIBUTES, permission,
RequestAttributes.SCOPE_REQUEST);
}

public static String getContext()
{
return Convert.toStr(RequestContextHolder.currentRequestAttributes().getAttribute(PERMISSION_CONTEXT_ATTRIBUTES,
RequestAttributes.SCOPE_REQUEST));
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 验证用户是否具备某权限
*
* @param permission 权限字符串
* @return 用户是否具备某权限
*/
public boolean hasPermi(String permission)
{
if (StringUtils.isEmpty(permission))
{
return false;
}
LoginUser loginUser = SecurityUtils.getLoginUser();
if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions()))
{
return false;
}
PermissionContextHolder.setContext(permission); //使用线程对象缓存
return hasPermissions(loginUser.getPermissions(), permission);
}
1
2
3
4
5
6
7
8
9
10
11
/**
* 判断是否包含权限
*
* @param permissions 权限列表
* @param permission 权限字符串
* @return 用户是否具备某权限
*/
private boolean hasPermissions(Set<String> permissions, String permission)
{
return permissions.contains(Constants.ALL_PERMISSION) || permissions.contains(StringUtils.trim(permission));
}