数据隔离
在实际开发中,需要设置用户只能查看哪些部门的数据,这种情况一般称为数据权限。
例如对于销售,财务的数据,它们是非常敏感的,因此要求对数据权限进行控制, 对于基于集团性的应用系统而言,就更多需要控制好各自公司的数据了。如设置只能看本公司、或者本部门的数据,对于特殊的领导,可能需要跨部门的数据, 因此程序不能硬编码那个领导该访问哪些数据,需要进行后台的权限和数据权限的控制。
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_id 和 user_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 "";
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
|
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, ""); } } @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
|
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>(); user.getRoles().forEach(role -> { if (DATA_SCOPE_CUSTOM.equals(role.getDataScope()) && StringUtils.containsAny(role.getPermissions(), Convert.toStrArray(permission))) { scopeCustomIds.add(Convert.toStr(role.getRoleId())); } });
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) { 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 { sqlString.append(StringUtils.format(" OR {}.dept_id = 0 ", deptAlias)); } } conditions.add(dataScope); }
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 2 3 4 5 6 7 8
| @PreAuthorize("@ss.hasPermi('system:user:list')")
@PreAuthorize("@ss.lacksPermi('system:user:list')")
@PreAuthorize("@ss.hasAnyPermi('system:user:add,system:user:edit')")
|
编程式判断是否有资源权限
1 2 3 4
| if (SecurityUtils.hasPermi("sys:user:edit")) { System.out.println("当前用户有编辑用户权限"); }
|
- 角色权限示例。
1 2 3 4 5 6 7 8
| @PreAuthorize("@ss.hasRole('user')")
@PreAuthorize("@ss.lacksRole('user')")
@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
提供了一些静态方法,可以帮助开发者方便地访问当前请求的上下文信息:
- 获取当前请求的
RequestAttributes
:
RequestContextHolder.getRequestAttributes()
: 返回当前线程的 RequestAttributes
,如果没有可用的请求上下文,则返回 null
。
- 设置当前线程的
RequestAttributes
:
RequestContextHolder.setRequestAttributes(RequestAttributes attributes)
: 设置当前线程的 RequestAttributes
。
RequestContextHolder.setRequestAttributes(RequestAttributes attributes, boolean inheritable)
: 设置当前线程的 RequestAttributes
,第二个参数指定是否应使这些属性可继承(即在子线程中是否可用)。
- 清除当前线程的
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
|
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
|
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
|
private boolean hasPermissions(Set<String> permissions, String permission) { return permissions.contains(Constants.ALL_PERMISSION) || permissions.contains(StringUtils.trim(permission)); }
|