# 有关权限设计的思考

## 2017年8月24日

最近看公司的代码，对权限管理颇有兴趣，恰好今天有空，就记录并总结了一下，如果有错误，还望指正。

通过慕课网的一些课程，一遍的权限管理的表结构如下：

![image-20210915100854997](https://2351062869-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F7b2CdwBN9liniVJpfEAc%2Fuploads%2Fgit-blob-9d040abeb1ab6283e5c49c35bf1d2002e7e7f30a%2Fimage-20210915100854997.png?alt=media)

从图上可以看出，表结构主要有`User`，`Role`，`Function` 三个表，分别对应用户、角色、功能，通过这三张表完成权限的控制和实现。这也是从传统的基于`RBAC`的角色权限空。

* User 与 Role 是多对多关系，使用第三方UserRole表来维护
* sRole 与 Function 同样也是多对多的关系，多个角色对应多个功能（每个模块都有对应的id，功能即模块，即menu），使用第三方表RoleFunction来维护

**大概思路：**

用户登陆时加载菜单，根据用户查询用户角色，根据用户角色查询用户功能，将查询到一个menu code，然后返回这些menu code

在控制层在根据menu code 获取并加载菜单。s

但是无法对每个menu 中的按钮权限进行控制（比如添加、删除、审核）

**解决办法：** 在Role和Function的第三方表中插入一列：`menu_btn`，代表该该角色下的功能拥有什么样的按钮权限

然后查询时，封装一个拥有menu\_btn的实体类menu，用来记录权限，如果没有任何button，则不显示任何菜单

***

**项目做法：**

```
表结构：

T_SYS_USER 用户表

T_SYS_ROLE 角色表

T_SYS_USER_ROLE 用户角色关系表

T_SYS_MENU 功能表

T_SYS_ROLE_PRIVILEGE 按钮权限 

[关于权限按钮列表]:

系统有固化的权限按钮,新增,修改,删除,查看,审核..

名称对应:ADD_BTN,MOD_BTN,DEL_BTN,VIEW_BTN,AUDIT_BTN...应:ADD_BTN,MOD_BTN,DEL_BTN,VIEW_BTN,AUDIT_BTN...
```

多个用户对应多个角色 ， 使用第三方表`T_SYS_USER_ROLE`维护

***

**权限管理中的授权问题：**

一个用户可以对应多个角色，A角色的操作权限和B角色的操作权限有冲突，那该以谁的为准？

**下面有几种解决方案：**

1. 合并权限，将A角色的权限和B角色的权限合并 √
2. 在视图上给用户选择，让用户可以切换角色，操作麻烦 ×
3. 如果角色之间有授权冲突，则不允许授权 ×
4. 允许用户对冲突角色进行授予，让用户在授予权限时，设置角色的优先级，当角色冲突时，以优先级高的角色为准 √

**公司代码分析：**

```java
// 定义一个map，用来保存菜单权限 
Map<String, MenuPrivilege> ret = new HashMap<String, MenuPrivilege>();
// 查询出权限并添加，使用sql直接查询menu，不查role，即不实例化role，即可避免这个问题，但是btns又拥有这个问题，待解决
List<SysRolePrivilege> priviletList = sysRolePrivilegeDao.findByUserRolePrivilege(userId);
for (SysRolePrivilege rp : priviletList) {  
  ret.put(rp.getMenuCode(), new MenuPrivilege(rp.getMenuCode(), rp.getMenuBtn()));
  // 相同的menu code会直接被map的唯一性覆盖
}
return ret;
```

问题解决，思路大概是按照解决方案一

**新问题： btns问题**

`A role` 对应 `A menu` - `btn_add, btn_query`

`B role` 对应 `A menu` - `btn_add, btn_delete`

如果Arole和Brole都赋给User1那么权限以谁的为准？

公司代码在这部分并没有处理，有可能是代码的bug，个人认为这个地方使用合并的方式比较合适。

**关于菜单管理：**

项目的权限管理是根据当前用户的类型，决定要显示的菜单 从而实现权限管理的功能。当加载menu时，会在控制层根据当前登陆用户获取菜单:

```java
/* 功能菜单数据获取 */
@RequestMapping("tree")
@ResponseBody
public Object menuTree(HttpServletRequest request, HttpSession session) {
  ...
    // 从数据库中获取菜单数据
    List<SysMenu> list = menuService.menusOfUser(session);
  List<Map<String, String>> ret = new ArrayList<Map<String, String>>();
  for (SysMenu po : list) {
    Map<String, String> map = new HashMap<String, String>();
    map.put("id", po.getMenuCode());
    map.put("pid", po.getMenuPcode());
    map.put("name", po.getMenuName());
    if (po.isLeafYn()) { // 如果为叶子节点
      map.put("url", "menu/jump/" + po.getMenuCode()); // 点击链接
    } else {
      map.put("url", po.getMenuUrl());
    }
    ret.add(map);
  }
  return ret;   // 这里在mvc配置文件中配置了json转换器，spring mvc会自动帮你将集合转换为json字符串，发送给jsp，使用js进行菜单的显示
}
```

```
List<SysMenu> list = menuService.menusOfUser(session);
```

`menusOfUser`方法：

```java
@Service("menuService") 
public class MenuService {
  public List<SysMenu> menusOfUser(HttpSession session) {
    SessionUser sessionUser = (SessionUser) session.getAttribute(SessionUser.SESSION_USER_KEY);
    Assert.notNull(sessionUser, "user information in session cannot be null");
    // 超级管理员
    if (SysUser.USER_TYPE_MASTER == sessionUser.getUserType()) {
      return menuList;
    }
    // 其他普通用户
    Map<String, MenuPrivilege> menus = sessionUser.getPrivilege();
    // 如果没有权限菜单，返回空
    if (menus == null || menus.size() == 0) {
      return new ArrayList<SysMenu>();
    }
    List<SysMenu> result = new ArrayList<SysMenu>();
    for (SysMenu sysMenu : menuList) {
      if (menus.containsKey(sysMenu.getMenuCode())) {
        result.add(sysMenu);
      }
    }
    return result;
  }
}
```

## 2019年9月12日

对权限有了新的理解，这家公司的权限系统相比较之前的设计的更成熟，同样是基于`RABC`的方式，但是不基于URL，基于对单个接口。

### 权限设计

在该系统中，每个http接口都称为一个交易`TRANS`，多个交易组成一个产品`PRODUCT`，而多个产品构成一个产品组`PRODUCTGROUP`。在给客户赋予权限时，会在后台管理系统中，将一个或者多个产品组/产品赋予客户。客户被赋予该产品的权限时，权限下的所有交易都是可访问的。

当用户登录时，会拉取所有已经授权的产品/产品组，并将这些产品/产品组对应的所有的交易(接口url)获取，并去重放置在用户session中。当用户访问某个交易时，会在责任链/顾虑器中判断此接口是否在用户的权限中，如果在放行，不在则会停止访问，告知用户无权限操作。

### 菜单设计

而关于多级菜单的控制，是统一放置在一张`RULE`表中，在表中包含如下几个信息，`RULEID`，`RULETYPE`，`RULEDEF`。比如有如下菜单：

```
用户
	用户管理
		添加用户
		删除用户
	用户配置
```

其数据在`RULE`的表现是这样的：

```
ruleid				ruletype				ruledef
root					menutree				user,product,xxx,...
user					menutree				usermanager, userconfig
usermanager 	menutree				useradd, userdelete
useradd				menu						prdid_useradd,user/add
userdelete		menu 						prdid_userdelete,user/delete
userconfig		menu 						prdid_userconfig, user/config
```

在项目中，会将所有`ruletype=menu or ruletype=menutype`的菜单数据取出，找到根菜单`root`，其中`menutree`代表该菜单不是叶子菜单，需要继续寻找其子菜单，子菜单的定义位于字段`ruledef`中。通过这种方式依次递归查找，当遇到类型为`menu`的数据时，说明菜单是叶子菜单，就停止向下层递归，并取出`ruledef`字段，这时，叶子菜单的`ruledef`字段，定义的信息就不再是子菜单了，而是该菜单对应的`product id`，而逗号后面的代表点击改菜单，需要跳转的接口地址。

当用户登录时，先取出所有产品信息，取出后，再生成菜单树，当用户没有菜单对应所需的产品的权限时，这个菜单就不会增加到菜单树中，故在页面上也无法获取到没有权限的菜单。

### 涉及的表

```
PRODUCTGROUP 		#产品组表
PRODUCT    			#产品表
PRODUCTGROUPPRODUCT #产品与产品组关联表
PRODUCTTRS 			#产品与交易关联表

RULE 						#规则表
PRODUCTUSER			#用户产品关联标
```

### 按钮权限

关于按钮权限的设计。在项目中，很少有对单个按钮的控制的，通常都是以菜单或者product作为控制权限。可以将某些按钮设置为一个产品，然后通过判断权限的方式（判断用户是否有次产品的权限），来决定是否展示相应的按钮或者地址。为了方便使用，项目还封装了一套`jsp`标签（老），以及前端组件（新、以及H5）。

此外，还可以通过对RULE表的控制来达到此效果。比如，在RULE表中定义要进行控制的RULEID，比如：

```
ruleid				ruletype				ruledef
useradd				button					
userdel				button	
```

然后通过表`USERRULE`，将用户可操作性的按钮写入，并通过共用组件判断用户是否有用该RULE实现。缺点是每一个要控制的按钮都要单独配置，并且与产品没有关联，管理起来较为困难，可适当的优化。

### rule的其他作用

`rule`中文名称称为规则，所以rule表的功能远远不止于此。例如：

* 在查询客户账户列表的功能中，制定了一套规则，根据用户的类型，可限制可操作账户的类型
* 定义了某些业务的开始时间与结束时间，有点像配置中心

### 优点

1. 权限控制严格
2. 结构层次清晰

### 缺点

1. 因为将菜单、权限等信息配置在数据库中，所以没在新增一个接口、菜单等信息时，都要对应插入一堆复杂的SQL，因此上线总是出现问题。并且当菜单数据出现问题时，会导致所有功能展示都有问题（比如，所有功能缺失）
2. rule表中的数据在项目启动时仅仅加载一次，好处是rule表中的数据频繁读取，缓存在内存中减少系统压力。缺点是每次更改菜单都要重启服务，造成服务不可用

   个人认为的解决办法：增加状态`SQL是否更改`，如果SQL发生更改，就重新读取SQL缓存；或者将数据缓存在redis或者其他缓存中间件中，然后通过缓存控制刷新
3. 菜单信息无法进行版本控制，这个可以通过`Flyway`实现。


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://yangsx95.gitbook.io/notes/domain-specific/an-quan-kuang-jia/you-guan-quan-xian-she-ji-de-si-kao.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
