RBAC权限管理

访问控制策略


访问策略 理解
DAC(自主型访问控制) 用户/对象来决定访问权限,信息的所有者设定谁有权限访问信息及操作,基于身份的访问控制,如 UNIX 权限管理
MAC(强制性访问控制) 系统决定访问权限,由操作系统的规则决定,基于规则
基于属性证书的访问控制 访问权限信息存放在用户属性证书的权限属性中
RBAC(Role-Based Access Control) 基于角色的访问控制,某个职位根据工作需要调整,RBAC模型对组织内部的关系和访问控制给出恰当的描述

RBAC 访问控制的思路


  1. 为用户分配角色和权限(用户、角色和权限数据及其之间关系数据存入数据库)
  2. 获取当前用户和被访问的资源
    • 若用户是管理员,直接放行访问;
    • 若资源不需要权限控制,直接放行访问;
    • 其他情况则从数据库中查询该用户(先找角色,再找角色对应的权限)是否具有访问该资源的权限,若没有,提醒用户权限不足,若有放行访问。

2019-08-18 at 9.49 P

先把所有 domain 独立的增删改查做完。然后将有关系的 domain 建立关系,并修改页面让其关联。比如 emloyee 与 department 多对一关系,employee 与 role 多对多关系

注意:权限表达式值必须唯一

权限表达式的生成


权限表达式必须唯一,用来区分用户访问的到底是什么资源。权限控制,就是对 Controller 中的处理方法做限制,因为处理方法包含数据库的 CRUD 操作,所以控制器中的一个个处理方法就是一个个的权限,所以数据库中,权限表达式就是所有控制器的一个一个的方法。

permission 表中,name 就是给角色分配时看的,必须见名知意。比如,非VIP用户无法访问付费区内容。

expression 必须为宜,可以用 控制器类名首字母小写:方法名,来设置。

可以使用注解,获取容器对象,从容器对象中获取所有贴 @Controller 注解的 bean,然后获取贴有自己定义的注解的方法,获取权限名称和权限表达式(可以手动拼接,也可以是用注解定义获取),

  1. 判断该方法是否有我们自定义的注解,
  2. 判断这个方法的权限表达式是否在数据库中,
  3. 如果有注解且权限表达式在数据库中查不到,就创建 Permission 对象,添加到数据库中。

权限管理实现


加载权限


自定义权限控制注解

1
2
3
4
5
@Target(ElementType.METHOD) // 该注解可以使用的目标
@Retention(RetentionPolicy.RUNTIME) // 哪个时期扫描注解
public @interface RequiredPermission {
String name();
}

Permission 的添加跟其他的添加不太一样,它是通过给 Controller 中的方法贴自定义注解来获取对象的。而不是手动去添加

修改权限管理中的添加按钮样式为加载:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script type="text/javascript">
$('#reload').click(function () {
$.post('/permission/reload', function (data) {
if (data.success) {

} else {
alert('系统繁忙')
}
})
})
})
</script>

<button id="reload" type="button" class="btn btn-primary">
<span class="glyphicon glyphicon-refresh" aria-hidden="true"></span>加载
</button>

贴注解

在 Controller 需要添加权限管理的方法贴上 @RequestPermission 注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 列表页
*/
@RequestMapping("/list")
@RequiredPermission(name = "员工列表")
public String list(Model model, @ModelAttribute("qe") QueryEmployee qe) {
if (qe == null || qe.getCurrentPage() == null) {
qe = QueryEmployee.EMPTYQUERY;
model.addAttribute("qe", qe);
}
QueryResult result = employeeService.gets(qe);
model.addAttribute("result", result);
return "employee/list";
}

实现 PermissionController 的 reload 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 扫描 @RequiredPermission 注解,加载贴有注解的权限方法
* @param model
* @return
*/
@RequestMapping("/reload")
@ResponseBody
public Map<String, Boolean> reload(Model model) {
Map<String, Boolean> result = new HashMap<>();
try {
permissionService.reload();
result.put("success", true);
} catch (Exception e) {
e.printStackTrace();
result.put("success", false);
}
return result;
}

service reload 方法

在 IPermissionService 中添加 reload 方法,以及实现类,获取 ApplicationContext 对象,为了获取容器中的 bean。

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
@Autowired
private ApplicationContext context;

@Override
public void reload() {
// 获取所有的 expression 集合,用于之后判断是否已经添加这个方法
List<Permission> permissions = permissionMapper.selectAll();
Set<String> permissionSet = new HashSet<>();
for (Permission permission : permissions) {
permissionSet.add(permission.getExpression());
}

// 1. 获取容器对象,从容器中获取所有贴有 @Controller 注解的 bean
Map<String, Object> beans = context.getBeansWithAnnotation(Controller.class);
Set<String> keys = beans.keySet();
for (String key : keys) {
System.out.println(beans.get(key));
System.out.println(key);
}
/**
* 打印:
* cn.lizhaoloveit.ssm.web.Controller.EmployeeController@7d695bb6
* employeeController
* ...
* 所有 @Controller 注解的 beans
*/
// 2. 获取贴有自定义的 @RequiredPermission 注解的方法
Collection<Object> controllers = beans.values();
for (Object controller : controllers) {
Method[] methods = controller.getClass().getDeclaredMethods();
// 获取所有的方法(包括 private),不包括父类
for (Method method : methods) {
boolean isPermissionMethod = method.isAnnotationPresent(RequiredPermission.class);
// 只打印贴了注解的方法
if (isPermissionMethod) {
String methodExpression = StringUtil.getMethodExpression(method);
if (permissionSet.contains(methodExpression)) {
// 说明数据库中已经存在了 methodExpression
continue;
}
Permission permission = new Permission();
permission.setName(method.getAnnotation(RequiredPermission.class).name());
permission.setExpression(methodExpression); // employee:delete
permissionMapper.insert(permission);
}
}
}
}

上面是实现思路,下面是使用时的代码

PermissionServiceImpl.java

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
@Override
public void reload() {
List<Permission> permissions = permissionMapper.selectAll();
Set<String> permissionSet = new HashSet<>();
for (Permission permission : permissions) {
permissionSet.add(permission.getExpression());
}

Map<String, Object> beans = context.getBeansWithAnnotation(Controller.class);
Collection<Object> controllers = beans.values();
for (Object controller : controllers) {
Method[] methods = controller.getClass().getDeclaredMethods();
for (Method method : methods) {
boolean isPermissionMethod = method.isAnnotationPresent(RequiredPermission.class);
if (isPermissionMethod) {
String methodExpression = StringUtil.getMethodExpression(method);
if (permissionSet.contains(methodExpression)) {
// 说明数据库中已经存在了 methodExpression
continue;
}
Permission permission = new Permission();
permission.setName(method.getAnnotation(RequiredPermission.class).name());
permission.setExpression(methodExpression); // employee:delete
permissionMapper.insert(permission);
}
}
}
}

StringUtil.java

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
/**
* 拼接全限定名表达式
* @param method
* @return
*/
public static String getMethodExpression(Method method) {
// 1. 获取该方法所在的类的类简单名 例:EmployeeController
String name = method.getDeclaringClass().getSimpleName();
// 2. 将 Controller 去掉,将 E 小写,变成 employee
name = name.replace("Controller", "");
name = firstToLowerCase(name);
return name + ":" + method.getName();
}

/**
* 首字母小写
* @param str
* @return
*/
public static String firstToLowerCase(String str) {
if (isNotEmpty(str)) {
return (str.charAt(0) + "").toLowerCase() + str.substring(1);
}
return "";
}

权限控制思路


  1. 区分请求是哪个用户发起
    • 用户访问之前,需要进行身份认证。
  2. 权限判断流程
    • 获取当前请求用户
    • 若用户时管理员,直接放行
    • 获取访问方法上是否需要权限控制,如果没有贴 @RequirePermission 注解,直接放行
    • 获取访问处理器方法,拼接权限表达式,从数据库获取该用户的权限数据,如果包含,则放行

用户岗位变化少,没必要每次都去数据库查询,浪费性能,第一次查询之后进行缓存,可以在登录时缓存权限数据。

具体代码:

PermissionInterceptor.java

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
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (!(handler instanceof HandlerMethod)) {
// 如果不是动态资源,就放行
return true;
}
// 1. 获取当前请求用户
Employee e = SessionUtil.getSessionAttribute(Employee.class);
// 2. 若用户是管理员直接放行
if (e.getAdmin()) {
return true;
}
// 3. 获取访问方法上是否需要权限控制,如果没有贴 @RequirePermission 注解,直接放行
HandlerMethod handlerMethod = (HandlerMethod) handler;

RequiredPermission methodAnnotation = handlerMethod.getMethodAnnotation(RequiredPermission.class);
if (methodAnnotation == null) {
return true;
}
// 4. 获取访问方法,拼接权限表达式,从 session 中找到对应的权限,如果有就放行。
String methodExpression = StringUtil.getMethodExpression(handlerMethod.getMethod());
Set<String> expressions = (Set<String>) SessionUtil.getSessionAttribute(Permission.class);
if (expressions.contains(methodExpression)) {
return true;
}
request.getRequestDispatcher("/common/nopermission.jsp").forward(request, response);
return false;
}

登录优化

详情见:Ajax实现登录

文章作者: Ammar
文章链接: http://lizhaoloveit.cn/2019/08/17/RBAC%E6%9D%83%E9%99%90%E7%AE%A1%E7%90%86/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Ammar's Blog
打赏
  • 微信
  • 支付宝

评论