原《第9章 认证与授权》节略部分
注意
这一章的节略内容需要结合Spring Security一起来看。
解析权限结构
权限设计是一件比较抽象的思考活动。有时候一些复杂的权限、角色、组织、用户等内容交织在一起,会让人觉得无从下手。不过只要通过适当的方法来解构,慢慢地抽丝剥茧,就不那么难做了。
例如,笔者比较喜欢用汉堡包
法来做权限设计。所谓汉堡包
法,顾名思义,就是权限系统像汉堡包那样直观、清晰。

汉堡包
中的上层组织结构
可能是这样的。

而中间部分的分组或角色可能是这样的。

下层权限集合又可能是这样的。

如果要做权限叠加的话就是这样的。

这里把和权限进行连线的图省略掉了,因为实在是太复杂了。不过刚开始接触权限系统的话,也不要被这种复杂性给吓到,即使是最复杂的权限也是从RBAC0进化来的,完全可以通过不断地实践,逐渐熟悉并熟练掌握它。
实现权限
前面把RBAC的来龙去脉及分析、设计方法撸了一遍,现在学以致用,结合Spring Security权限框架来实现它。
假定此处按照上面权限叠加
的图设计来实现自定义的权限系统,整个过程大致分这么几个步骤。
定义出完整的权限系统表结构。
实现
Entity
、Dao
、Service
等类代码。实现Spring Security自定义拦截器。
实现
Controller
,完成权限验证。
这里只展示核心代码,至于外围的Entity
、Dao
、Service
等都可以单独下载。
数据库中定义了机构、组、角色、权限、用户及其之间的关系,分别对应SysBranch
、SysGroup
、SysRole
、SysPermission
、SysUser
实体类。
除了SysBranch
外,它们也都有对应的Service
类,分别是GroupService
、RoleService
、PermissionService
、UserService
。
Spring Security的强大之处就在于它的拦截器,所以这里也参照它实现自己的权限拦截器。
package cn.javabook.chapter09.annotations;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 权限注解
*
*/
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface PreAuthorize {
// 组
String group() default "";
// 角色
String role() default "";
// 权限
String permission() default "";
}
拦截器需要完成两个任务。
查找用户拥有的资源,也就是要完成下面的工作。
找到某个用户所属的所有组(不需要去找这些组的父组)。
找到某个用户拥有的所有角色(同时要逐个找到所有这些角色的父角色)。
找到某个用户拥有的所有权限。
将权限与资源做比对,确认是否对该资源有访问权限。
有了注解之后,再来定义拦截处理器。
package cn.javabook.chapter09.security;
import cn.javabook.chapter09.annotations.PreAuthorize;
import cn.javabook.chapter09.entity.SysPermission;
import cn.javabook.chapter09.entity.SysUser;
import cn.javabook.chapter09.service.PermissionService;
import cn.javabook.chapter09.service.RoleService;
import cn.javabook.chapter09.service.UserService;
import cn.javabook.chapter09.entity.SysRole;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.util.DigestUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.*;
/**
* 拦截处理器
*
*/
@Aspect
@Component
public class InterceptorHandler {
@Resource
private UserService userService;
@Resource
private RoleService roleService;
@Resource
private PermissionService permissionService;
/*
* 拦截controller包下面的所有类中,有@RequestMapping注解的方法
*/
@Pointcut("execution(* cn.javabook.chapter04.controller..*.*(..)) " +
"&& @annotation(org.springframework.web.bind.annotation.RequestMapping)")
public void controllerMethodPointcut() {
}
/**
* 拦截器具体实现
*/
@Around("controllerMethodPointcut()")
public Object Interceptor(final ProceedingJoinPoint pjp) {
ServletRequestAttributes sra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = sra.getRequest();
Map<String, String> argsMap = new HashMap<String, String>();
Enumeration<String> em = request.getParameterNames();
String methodName = pjp.getSignature().getName();
// 加入参数
while (em.hasMoreElements()) {
String paramName = em.nextElement();
String value = request.getParameter(paramName);
argsMap.put(paramName, value);
}
String username = argsMap.get("username");
String platform = argsMap.get("platform");
String timestamp = argsMap.get("timestamp");
String signature = argsMap.get("signature");
// 验证参数
if (null == platform || null == timestamp || null == signature) {
return "params required";
}
// 后端生成签名:platform = "javabook", timestamp = "159123456789", signature = a8354bc1b54a39528e81c549ec373c14
String sign = DigestUtils.md5DigestAsHex((platform + timestamp).getBytes());
if (!signature.equalsIgnoreCase(sign)) {
return "signature error";
}
// 获取切面标记
Signature signatureObject = pjp.getSignature();
if (!(signatureObject instanceof MethodSignature)) {
throw new IllegalArgumentException("this annotation can be applied on method only");
}
// 获取用户信息
SysUser user = userService.queryByUsername(username);
if (null == user) {
return "user is not exist";
}
/*
* 获得用户拥有的全部角色
*
*/
// 用户所属的角色
Set<SysRole> userRoleSet = new HashSet<>();
// 用户所拥有的全部角色
Set<SysRole> userAllRoleSet = new HashSet<>();
// 查询这些角色的全部父角色
StringBuilder roleIds = new StringBuilder();
Set<String> userRoleNameSet = new HashSet<>();
// 用户-组-角色
List<SysRole> ugr = roleService.queryUGRByUserId(user.getId());
if (null != ugr && ugr.size() > 0) {
userRoleSet.addAll(ugr);
}
// 用户-角色
List<SysRole> ur = roleService.queryURByUserId(user.getId());
if (null != ur && ur.size() > 0) {
userRoleSet.addAll(ur);
}
// 合并全部角色
for (SysRole role : userRoleSet) {
List<SysRole> list = roleService.queryParentsById(role.getParentids());
if (null != list && list.size() > 0) {
list.forEach(r -> {
userAllRoleSet.add(r);
userAllRoleSet.add(role);
});
}
}
// 查询这些角色的全部权限
userAllRoleSet.forEach(r -> {
roleIds.append(r.getId()).append(",").append(r.getParentids());
userRoleNameSet.add(r.getName());
});
List<SysPermission> rolePermissions = permissionService.queryByMultiRoleIds(roleIds.toString());
/*
* 获得用户拥有的全部权限
*
*/
Set<String> userPermissionSet = new HashSet<>();
// 用户-组-角色-权限
List<SysPermission> ugrp = permissionService.queryUGRPByUserId(user.getId());
if (null != ugrp && ugrp.size() > 0) {
ugrp.forEach(r -> userPermissionSet.add(r.getPath()));
}
// 用户-角色-权限
List<SysPermission> urp = permissionService.queryURPByUserId(user.getId());
if (null != urp && urp.size() > 0) {
urp.forEach(r -> userPermissionSet.add(r.getPath()));
}
// 用户-权限
List<SysPermission> up = permissionService.queryUPByUserId(user.getId());
if (null != up && up.size() > 0) {
up.forEach(r -> userPermissionSet.add(r.getPath()));
}
// 合并之前的结果
if (null != rolePermissions && rolePermissions.size() > 0) {
rolePermissions.forEach(r -> userPermissionSet.add(r.getPath()));
}
// 权限判断
Object target = pjp.getTarget();
Class<?> clazz = target.getClass();
MethodSignature methodSignature = (MethodSignature) signatureObject;
try {
Method method = target.getClass().getMethod(methodSignature.getName(), methodSignature.getParameterTypes());
// 获取注解类
PreAuthorize preAuthorize = method.getDeclaredAnnotation(PreAuthorize.class);
RequestMapping requestMapping = method.getDeclaredAnnotation(RequestMapping.class);
if (null != preAuthorize) {
// 只有当用户具备此角色且访问路径与权限中的path相等时才能认为具有该资源的操作权限
if (userRoleNameSet.contains(preAuthorize.role()) && userPermissionSet.contains(requestMapping.value()[0])) {
System.out.println(username + " ==> " + clazz.getSimpleName() + " - " + method.getName() + " - " + preAuthorize.role() + " - " + requestMapping.value()[0]);
// 继续往下执行
return pjp.proceed();
} else {
return "permission denied";
}
}
} catch (Throwable e) {
e.printStackTrace();
}
return "failure";
}
}
InterceptorHandler
类做了下面几件事。
首先查询用户信息,如果用户不存在则直接返回。
把用户所拥有的角色及其父角色全部查出来,再合并到集合
Set<SysRole>
的变量中。再把这些角色所用的全部权限查出来,存放到集合
List<SysPermission>
的变量中。查询直接给用户、组分配权限,并且把它们全部放在
Set<String>
的变量中。将集合
List<SysPermission>
和集合Set<String>
中的权限合并。解析拥有注解
@PreAuthorize的
方法,判断用户是否拥有某个角色、组、权限。如果有则可以顺利访问,否则返回permission denied
。
UserController
中的接口是通过InterceptorHandler
类和@PreAuthorize
注解起作用的。
package cn.javabook.chapter09.rbac.controller;
import cn.javabook.chapter09.rbac.annotations.PreAuthorize;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
/**
* 用户Controller
*
*/
@RestController
public class UserController {
/**
* 接口访问:
* 用例一:清漪(uid=7)->组(无)->角色(产品,rid=5),结果:无权限
* 用例二:姜立(uid=9)->组(无)->角色(客服,rid=4),结果:正常访问
* 用例三:石昊(uid=3)->组(gid=10001)->角色(客服、产品、运营,rid=4,5,6),结果:正常访问
*
*/
@PreAuthorize(role = "客服")
@RequestMapping(value = "/api/v1.0.0/user/details", method = RequestMethod.GET)
public String details(String username) {
return username + " 有查看用户详情的权限";
}
/**
* 接口访问:
* 用例四:柳神(uid=4)->组(gid=10002)->角色(会计、出纳、库管、配送,rid=7,8,9,10),结果:无权限
*
*/
@PreAuthorize(role = "产品")
@RequestMapping(value = "/api/v1.0.0/system/setting/password", method = RequestMethod.GET)
public String password(String username) {
return username + " 有修改密码的权限";
}
}
可以按照代码注释中的用例一
至用例四
,用Postman试试效果,当然自己也尝试可以制造更多的用例看看权限系统是否管用。
相关的用户信息在数据库初始化时已插入sys_user
表。
另一种方式
通过Spring Security参照框架实现的注解非常方便,但有一个很大的问题,就是某些接口或方法的角色、组、权限直接在代码里面被写死了。
@PreAuthorize(role = "客服")
@RequestMapping(value = "/api/v1.0.0/user/details", method = RequestMethod.GET)
public String details(String username) {
return username + " 有查看用户详情的权限";
}
上面的接口/api/v1.0.0/user/details
如果想调整所属的角色,将不得不修改代码,这非常不友好。
现实中的权限更多时候是通过勾选的方式实现的,就像下面这样。

这种实现角色、权限分配的方式真正难的地方不是角色、组、权限等如何存储,而在于右边红色方框的权限树如何实现。
也就是怎么能够 把数据库中存储的数据表变成树型菜单,就是把下面这张图里面的数据表变成上面的树型菜单。

在这里提供一种思路,它是将数据表结构变为树型结构的关键代码。
public class Menu {
private String id; // 菜单编号
private Menu parent; // 父级菜单
private String parentIds; // 所有父级编号
private String name; // 名称
private String level; // 层级
private String path; // 路径
private List<Menu> childList = Lists.newArrayList();// 拥有子菜单列表
private List<Role> roleList = Lists.newArrayList(); // 所属角色列表
// getter、setter
......
public static void getTree(List<Menu> list, List<Menu> sourcelist, String parentId) {
for (int i = 0; i < sourcelist.size(); i++) {
Menu e = sourcelist.get(i);
if (e.getParent() != null && e.getParent().getId() != null && e.getParent().getId().equals(parentId)) {
list.add(e);
// 判断是否还有子节点, 有则继续获取子节点
for (int j = 0; j < sourcelist.size(); j++) {
Menu child = sourcelist.get(j);
if (child.getParent() != null && child.getParent().getId() != null && child.getParent().getId().equals(e.getId())) {
getTree(list, sourcelist, e.getId());
break;
}
}
}
}
}
}
很明显,getTree()
是一个递归方法,它接收三个参数。
第一个参数
List<Menu> list
,是最终要输出成树型菜单的一个输出参数。由递归方法产生的树型菜单 第二个参数
List<Menu> sourcelist
,是从数据库中读取出来的数据项集合,它是按照行集RowSet
的方式组织的。第三个参数
String parentId
表示树型列表是从哪个菜单节点开始的。一般情况下,如果parentId = 0
表示从Root
根节点开始组织树型菜单。
上面的代码就是整个可分配的权限系统中最为核心和关键的部分,至于列表、查询、修改、勾选、保存都只不过是非常简单的CRUD,没有任何技术含量。
说明
这里顺便提一句,有些技术课程将权限系统讲的天花乱坠,把诸如Session/Cookie机制、数据加密、Spring Spring Security源码解析、认证流程分析、权限校验流程、记住我(remember-me)
机制、过滤器等一大堆杂七杂八的东西都加了进去。
这些东西看起来好像很高大上,但其实和真正的权限系统压根就没啥关系,充其量只是一些外围辅助而已。
RBAC就那么点东西,其核心无非就是进行权限比对(跟用不用注解无关),再要高级一点就是分配权限菜单(树型递归实现)。
如果能把这些都搞得明明白白,那可以说几乎没有任何RBAC的分析、设计和开发能够难得倒。
感谢支持
更多内容,请移步《超级个体》。