基于spring boot的一个小项目,前后端分离,采用REST风格的接口设计,在做权限设计时觉得以前的权限控制实现比较繁琐,一个Action就对应一条Permission。所以想着接口可以走REST风格,权限为什么不可以呢?比如称作READ风格的权限控制(Representational Authorization Design)……  
REST早已不是新鲜事物,基于REST的权限控制网上也是有现成的,基于RESTful API 怎么设计用户权限控制?写得很棒,所以基于这篇文章的设计思路做了个实现(java)。本文的前半段,主要是引用文章阐述设计思路,后半部分主要为程序code实现。
其中后端自定义一个过滤器做权限过滤,用到了spring util包下的AntPathMatcher做权限规则匹配;前端也就一个directive就搞定了。技术上并没有引用什么技术框架,关键还是抽象思维且将设计思路转化为程序上code。
传统的权限配置 vs 权限被抽象成资源和状态  
| 12
 3
 4
 5
 
 | /getUsersGroups?id=1/getUsersExpress
 /getUsersLables?eventId=xxx
 /createUsers?name=xxx&age=18
 /updateEvent?id=xx
 
 | 
vs
| 12
 3
 
 | GET /users/**  表示 对 users 资源 及events 附属的资源 有获取/浏览的权限POST /users    表示 对 users 资源 有新增的权限
 PUT /users/*   表示 对 users 有 修改的权限
 
 | 
基础概念
RBAC:基于角色的访问控制
RBAC(Role-Based Access Control,基于角色的访问控制),就是用户通过角色与权限进行关联。简单地说,一个用户拥有若干角色,每一个角色拥有若干权限。这样,就构造成“用户-角色-权限”的授权模型。在这种模型中,用户与角色之间,角色与权限之间,一般者是多对多的关系。(如下图,图片来源)
用户 n:1/n 角色; 角色 n:n 权限

REST:资源和状态转换
(该节内容来源基于RESTful API 怎么设计用户权限控制?)
Representational State Transfer,简称REST,是Roy Fielding博士在2000年他的博士论文中提出来的一种万维网软件架构风格,目的是便于不同软件/程序在网络(例如互联网)中互相传递信息。  
REST比较重要的点是资源和状态转换,
所谓”资源”,就是网络上的一个实体,或者说是网络上的一个具体信息。它可以是一段文本、一张图片、一首歌曲、一种服务,总之就是一个具体的实在。
而 “状态转换”,则是把对应的HTTP协议里面,例如四个表示操作方式的动词分别对应四种基本操作:
- GET,用来浏览(browse)资源
- POST,用来新建(create)资源
- PUT,用来更新(update)资源
- DELETE,用来删除(delete)资源
  
资源的分类及操作
清楚了资源的概念,然后再来对资源进行一下分类,我把资源分为下面三类:
- 私人资源 (Personal Resource )
- 角色资源 (Roles Resource )
- 公共资源 (Public Resource )
“私人资源”:是属于某一个用户所有的资源,只有用户本人才能操作,其他用户不能操作。例如用户的个人信息、订单、收货地址等等。
“角色资源”:与私人资源不同,角色资源范畴更大,一个角色可以对应多个人,也就是一群人。如果给某角色分配了权限,那么只有身为该角色的用户才能拥有这些权限。例如系统资源只能够管理员操作,一般用户不能操作。
“公共资源”:所有人无论角色都能够访问并操作的资源。
而对资源的操作,无非就是分为四种:
- 浏览 (browse)
- 新增 (create)
- 更新 (update)
- 删除 (delete)
策略/过滤器
工作原理是大同小异的,主要是在一条HTTP请求访问一个Controller下的action之前进行检测。所以在这一层,我们可以自定义一些策略/过滤器来实现权限控制。
下面排版顺序对应Policy的运行顺序
- SessionAuthPolicy:检测用户是否已经登录,用户登录是进行下面检测的前提。
- ResourcePolicy:检测访问的资源是否存在,主要检测Source表的记录
- PermissionPolicy:检测该用户所属的角色,是否有对所访问资源进行对应操作的权限。
- OwnerPolicy:如果所访问的资源属于私人资源,则检测当前用户是否该资源的拥有者。
如果通过所有policy的检测,则把请求转发到目标action。

READ风格权限设计的实现
DB 设计
系统的业务场景比较简单,用户只需要一个角色即可,角色和权限还是多对多

接口&权限表
接口举例:

权限表举例:这块我把 资源对象 和权限表进行了合并,Resource 为parent_code =0 ,Permission 为parent_code!=0的,资源的权限是parent_code做区分。
其中url 是,Ant风格的pattern
| 12
 3
 4
 5
 
 | 字符wildcard    描述
 ?         匹配一个字符
 *         匹配0个及以上字符
 **         匹配0个及以上目录directories
 
 | 

权限控制代码实现(java-spring)
用户登录后,会从后台取出所有权限集合 和 用户权限集合,会根据这两个集合进行处理,一个是处理为后端需要的权限集合,后续拦截器根据这份缓存进行权限拦截;第二个是会给出一份给到前端,前端根据这份权限信息,对页面元素进行disabled或hidden处理
初始化权限集合:
登录后,初始化权限集合,且分别转换为 前端所需要的结构和后端所需要的权限结构
后端权限集合结构,这个集合为全量的权限集合,只是在原权限的基础上,增加一个字段,用户对该资源的某某操作有权限,则给这条记录打上1,否则为0。
| 12
 3
 4
 
 | 权限A urlA GET 1  权限B urlA POST  1
 权限C urlC GET  0
 权限D urlD GET  1
 
 | 
前端权限集合,给的也是全量的权限集合,不过不是列表形式,而是转化为了对象-权限的json 格式,也方便前端对页面元素的处理(判断0,1)
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 
 | "permissions": {"books": {
 "create": 1,
 "update": 1,
 "share": 1,
 "delete": 1,
 "browse": 1
 },
 "account": {
 "resetPwd": 1,
 "create": 0,
 "update": 1,
 "delete": 1,
 "browse": 1
 }
 },
 
 | 
用户登录,获取全量权限和用户权限,分别对其进行前后端结构转化,再存入缓存中(session,redis…)。别跟我提rest为啥用session,你们可以用token(jwt…)
| 12
 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
 
 | @ApiOperation(value = "用户登录", notes = "用户登录")@ApiImplicitParam(name = "model", value = "密码,请传蜜文", required = true, paramType = "body",
 dataType = "LoginUserVo")
 @RequestMapping(value = "/login", method = RequestMethod.POST)
 public BaseResult<Map<String, Object>> doLogin(@RequestBody LoginUserVo model) throws ParameterException {
 String username = model.getUsername();
 String password = model.getPassword();
 BaseUserInfo baseUserInfo = authService.getAuthUser(username, password);
 boolean flag = false;
 Map<String, Object> result = new HashMap<String, Object>();
 
 if (baseUserInfo != null) {
 flag = true;
 result.put("userinfo", baseUserInfo);
 session.setAttribute(Constant.LOGIN_USERINFO, baseUserInfo);
 
 
 List<BasePermission> allPermissions = authService.getAllPermissions();
 List<BasePermission> userPermissions = authService.getPermissionListByRoleId(baseUserInfo.getRoleid());
 
 
 List<PermissionVm> permissionVms = authService.transToBackendPermissions(allPermissions, userPermissions);
 session.setAttribute(Constant.SESSION_CURRENT_BACK_PERMISSION, permissionVms);
 
 
 Map<String, Map<String, Integer>> permissions
 = authService.transToFrontendPermissions(allPermissions, userPermissions);
 if (permissions == null) {
 permissions = new HashMap<>();
 }
 session.setAttribute(Constant.SESSION_CURRENT_FRONT_PERMISSION, permissions);
 result.put("permissions", permissions);
 }
 result.put("success", flag);
 return BaseResult.getInstance(ResponseCode.SUCCESS_0, result);
 }
 
 | 
转化为后端所需要的格式:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 
 | @Overridepublic List<PermissionVm> transToBackendPermissions(List<BasePermission> allPermissions, List<BasePermission> userPermissions) {
 List<PermissionVm>  permissionVms = new ArrayList<>();
 if (allPermissions == null
 || allPermissions.size() == 0 ) {
 return permissionVms;
 }
 
 allPermissions.stream().forEach(p -> {
 
 boolean isContain = userPermissions ==null ? false: userPermissions.stream()
 .filter(x -> p.getId().longValue() == x.getId().longValue())
 .findAny().isPresent();
 PermissionVm model = new PermissionVm();
 model.setId(p.getId());
 model.setParentCode(p.getParentCode());
 model.setCode(p.getCode());
 model.setUrl(p.getUrl());
 model.setMethod(p.getMethod());
 
 model.setContain(isContain ? (byte) 1 : (byte)0);
 permissionVms.add(model);
 });
 return permissionVms;
 }
 
 | 
转化为前端所需要的格式:
| 12
 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
 
 | @Overridepublic Map<String, Map<String, Integer>> transToFrontendPermissions(
 List<BasePermission> allPermissions,
 List<BasePermission> permissions) {
 Map<String, Map<String, Integer>> returnMap = new HashMap<>();
 if (allPermissions == null
 || allPermissions.size() == 0) {
 return null;
 }
 
 List<BasePermission> resources = allPermissions.stream()
 .filter(p -> Constant.PERMISSION_RESOURCE_TYPE.equals(p.getParentCode()))
 .collect(Collectors.toList());
 
 
 for (BasePermission resource : resources) {
 if (resource.getId() != null && resource.getCode() != null
 && StringUtils.isNotEmpty(resource.getCode())) {
 
 
 List<BasePermission> _all_permissions = allPermissions.stream()
 .filter(p -> resource.getCode().equals(p.getParentCode()))
 .collect(Collectors.toList());
 
 List<String> _user_permissions = new ArrayList<>();
 if(permissions != null){
 _user_permissions = permissions.stream()
 .filter(p -> resource.getCode().equals(p.getParentCode()))
 .map(p -> p.getCode()).collect(Collectors.toList());
 }
 
 Map<String, Integer> _singlePermission = new HashMap<>();
 for (BasePermission p : _all_permissions) {
 if (_user_permissions.contains(p.getCode())) {
 _singlePermission.put(p.getCode(), 1);
 } else {
 _singlePermission.put(p.getCode(), 0);
 }
 }
 returnMap.put(resource.getCode(), _singlePermission);
 }
 }
 return returnMap;
 }
 
 | 
权限拦截器:
主要实现了 1.SessionAuthPolicy:检测用户是否已经登录,用户登录是进行下面检测的前提。3. PermissionPolicy:检测该用户所属的角色,是否有对所访问资源进行对应操作的权限。逻辑参考注释。
| 12
 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
 
 | @Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
 String uri = request.getRequestURI();
 String method = request.getMethod();
 logger.info("AuthPermissionPolicy uri:" + uri + " -- method:" + method);
 
 HttpSession session = request.getSession();
 try {
 BaseUserInfo userInfo = (BaseUserInfo) session.getAttribute(Constant.LOGIN_USERINFO);
 if (userInfo != null) {
 
 
 
 String _url = uri.replace(contextPath, "");
 
 List<PermissionVm> permissions = (List<PermissionVm>) session.getAttribute(Constant.SESSION_CURRENT_BACK_PERMISSION);
 
 
 
 
 List<PermissionVm> _perms = permissions.stream()
 .filter(p -> StringUtils.isNotEmpty(p.getMethod())
 && p.getMethod().equals(method)
 && StringUtils.isNotEmpty(p.getUrl())
 && AntPathMatcherUtil.testUrl(p.getUrl(), _url)).collect(Collectors.toList());
 
 
 if (_perms == null || _perms.size() == 0) {
 return true;
 }
 
 
 
 
 if (_perms.size() > 1) {
 logger.warn("出现权限判别 重复的url:" + uri + ",method:" + method);
 _perms.forEach(p -> logger.warn("出现权限判别 重复的权限集合:"
 + ":" + p.getCode() + ":" + p.getUrl() + ":" + p.getMethod()));
 }
 
 for (PermissionVm pvm : _perms) {
 if (pvm.getContain() == (byte) 1) {
 return true;
 }
 }
 BusinessUtil.sendFailedMessage(response, ResponseCode.CODE_104_UN_AUTH);
 return false;
 }
 
 BusinessUtil.sendFailedMessage(response, ResponseCode.CODE_106_SESSION_TIMEOUT);
 
 } catch (Exception e) {
 logger.error("AuthPermissionPolicy-exp发生异常", e);
 }
 return false;
 }
 
 | 
其中判断request.url 是否符合在权限表配置好的pattern
主要用到了Spring自带的 的org.springframework.util.AntPathMatcher
| 12
 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
 
 | 
 
 
 
 
 public class AntPathMatcherUtil {
 private static final PathMatcher MATCHER = new AntPathMatcher();
 
 
 
 
 
 
 
 public static boolean testUrl(String pattern, String url){
 if(StringUtils.isNotEmpty(pattern) && StringUtils.isNotEmpty(url)){
 return MATCHER.match(pattern,url);
 }
 return false;
 }
 
 
 
 
 
 
 
 public static boolean isInExcludeUrlSet(Set<String> urlPatternSet, String url){
 
 if(urlPatternSet ==null || urlPatternSet.size() ==0){
 return false;
 }
 for (String pattern : urlPatternSet) {
 if(MATCHER.match(pattern,url)){
 
 return true;
 }
 }
 return false;
 }
 }
 
 | 
前端页面的权限控制
vue-directive 创建:
| 12
 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
 
 | import Vue from 'vue'import io from '@/module/io'
 import { get } from 'lodash'
 
 let permission = {}
 
 export default {
 
 get (path) {
 
 if (path) return get(permission, path, 0)
 return permission
 },
 
 // 获取权限列表。登录后,在页面加载时调用,获取权限列表
 requestPermission () {
 return new Promise((resolve, reject) => {
 io.get('session/permission').then(res => {
 let data = res.datas
 permission = data
 this.directive()
 resolve(data)
 })
 })
 },
 
 _setAttr (el) {
 let dom = el
 dom.setAttribute('disabled', 'disabled')
 dom.title = '当前操作无权限'
 },
 
 
 directive () {
 let _this = this
 Vue.directive('ps', {
 bind: function (el, binding, vnode) {
 let path = binding.arg ? binding.arg.replace(/-/g, '.') : null,
 value = _this.get(path),
 tagName = el.tagName.toLowerCase()
 if (value !== 1) tagName === 'button' || tagName === 'input' ? _this. _setAttr(el) : el.style.display = 'none'
 }
 })
 }
 }
 
 | 
登录成功后初始化获得权限集:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 
 | import permission from '@/module/permission'
 new Vue({
 methods: {
 initApp () {
 permission.requestPermission()
 
 },
 
 },
 created () {
 this.initApp()
 }
 })
 
 | 
页面元素处理:
资源:account 权限:brower。如果该用户没有account的浏览权限,则对该对象做el.style.display = 'none'操作
| 1
 | <a href="/#/user/account" v-ps:account-browse><span>成员管理</span></a>
 | 
资源:account 权限:create。如果获得的权限信息中 account:{create:0} 则对该button 做disabled。
| 1
 | <v-button v-ps:account-create type="primary" shape="circle" @click="addEvent">添加成员</v-button>
 | 
总结
对程序员最大的挑战,并不是能否掌握了哪些编程语言,哪些软件框架,而是对业务和需求的理解,然后在此基础上,把要点抽象出来,写成计算机能理解的语言。
参考
基于RESTful API 怎么设计用户权限控制?