前言
权限管理是每个系统不可缺少的一部分,大部分开发者应该都设计过权限管理系统,很多开发者学习的第一个项目可能就是权限管理系统。但是常见的权限设计在租户量非常大、角色数量非常多时会存在角色权限表数据量指数增长的情况,本文介绍一种可以避免这种情况的权限设计思路。
传统权限设计方案
传统的权限系统设计一般有四张表分别为 菜单表、角色表、角色菜单表、用户角色表,我们先按传统权限系统设计一套数据表结构:
菜单表 SYS_MENU
角色菜单 SYS_ROLE_MENU
用户角色 SYS_USER_ ROLE
数据指数增长问题
如果我们系统有 1 万个租户,每个租户有 100 个角色,每个角色有 100 个菜单点,则 SYS_ROLE_MENU 数据量为 1 亿条数据,这个数据是非常恐怖的。
新的权限设计
角色表 SYS_ROLE
对角色的菜单点进行编码,我们先构建一个二进制,默认为全 0,将对角色拥有的菜单 MENU_ID 位置为 1,如 管理员角色三个菜单权限们,它的的 MENU_ID 为 [16,10,3],则我们将第 16 位、第 10 位、第 3 位置成 1,则二进制编码为(从第 0 位开始)10000010000001000,我们将此二进制转成 36 进进制为 1fd4,二进制如下图所示
按上面的表设计后,我再看: 如果我们系统有 1 万个租户,每个租户有 100 个角色,每个角色有 100 个菜单点,则 SYS_ROLE 数据量为 100 万,比传统的少了 100 倍
与前端数据交换
用户登录后,前端会调用后端接口获取用户所能访问的菜单权限,比如用户有[16,10,3]菜单权限位,我数据库里存的是 36 位的编码 10g4,传给前端肯定要转成[16,10,3],这里我们利用 BigInteger 很易容就可以转成 36 进制,因为 BigInteger 最高进制只能支持 36 进制,可以自己写个简单的进制转换,转成 64 进制,这样随着 MENU_ID 增大,MENU_CODE 长度会小很多。
public class MenuCodeConvert{
/** * code 为36进制 String * 1fd4 返回 [16,10,3] * * @param code * @return */ public static List<Long> codeToIds(String code) { List<Long> ids = new ArrayList<>(); BigInteger bigInteger = new BigInteger(code, 36); for (int i = 0; i < bigInteger.bitLength(); i++) { if (bigInteger.testBit(i)) { ids.add((long) i); } } return ids; }
/** * [16,10,3] 编码 1fd4 * * @param ids * @return */ public static String idsToCode(List<Long> ids) { if (ids == null && ids.size() == 0) { return null; } BigInteger bigInteger = BigInteger.ZERO; for (Long id : ids) { bigInteger = bigInteger.setBit(id.intValue()); } return bigInteger.toString(36); }
public static void main(String[] args) { List<Long> ids = Arrays.asList(16L,10L,3L ); System.out.println(idsToCode(ids)); System.out.println(codeToIds("1fd4")); } //输出 1fd4 [3, 10, 16]
}
复制代码
为了 前端<-->后端<-->数据库双向传输过程 menuCode 编码转换变的更自动简单,我们可以简单封装一下,自定义 TypeHandler 可以解决此问题,可以参数我之前的文章 1。 首先创建一个 MenuCode 对象
public class MenuCode { public MenuCode(List<Long> appMenuIds) { this.appMenuIds = appMenuIds; this.menuCode = MenuCodeConvert.idsToCode(appMenuIds); } public MenuCode(String menuCode) { this.appMenuIds = MenuCodeConvert.codeToIds(menuCode); this.menuCode = menuCode; } private String menuCode; private List<Long> appMenuIds; public String getMenuCode(){ return menuCode; }
public void setMenuCode(String menuCode) { this.menuCode = menuCode; this.appMenuIds = MenuCodeConvert.codeToIds(menuCode); } public void setAppMenuIds(List<Long> appMenuIds) { this.appMenuIds = appMenuIds; this.menuCode = MenuCodeConvert.idsToCode(appMenuIds); }}
复制代码
创建角色表
@Data@TableName("sys_role")public class SysRole { @TableId private Long id; private String roleName; private MenuCode menuCode;}
复制代码
给 MenuCode 创建 TypeHandler
@MappedTypes({MenuCode.class})public class MenuCodeHandler extends BaseTypeHandler<MenuCode> { @Override public void setNonNullParameter(PreparedStatement ps, int i, MenuCode parameter, JdbcType jdbcType) throws SQLException { ps.setString(i, parameter.getMenuCode()); }
@Override public MenuCode getNullableResult(ResultSet rs, String columnName) throws SQLException { String content = rs.getString(columnName); return rs.wasNull() ? null : new MenuCode(content); }
@Override public MenuCode getNullableResult(ResultSet rs, int columnIndex) throws SQLException { String content = rs.getString(columnIndex); return rs.wasNull() ? null : new MenuCode(content); }
@Override public MenuCode getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { String content = cs.getString(columnIndex); return cs.wasNull() ? null : new MenuCode(content); }}
复制代码
这样我们在调用 SysRole sysRole=sysRoleService.getById(1L);从数据库查出的 36 进制编码 1fd4 会自动转成[16,10,3], 返回给前端的数据就是 JSON 格式为:
{ "id": 1, "name": "管理员", "menuCode": [ 16, 10, 3 ]}
复制代码
在保存角色权限时前端传的 JSON,我们调用 sysRoleService.save(sysRole)也会自动将[16,10,3]转成 1fd4 保存到数据库,这样完成自动转换,根本不用关心中间的菜单权限编码了。
总结
本文介绍一种适用于大量租户大量角色的权限系统设计,解决了系统由于租户数量及角色数据不断增长导致角色权限表成指数增长的问题,并巧妙利用 BigInteger 完成二进制和 36 进制中间的转换,最后利用 Mybatis 中的自定义 TypeHandler 解决前端到后端再到数据库菜单编码自动转换的问题。
缺点及未来展望
如果系统中菜单有 1000 个 ID 从 1-1000,某一个角色只有菜单 ID 为 1000 的权限点,那么他的 menuCode 为
4lxcmkxpcdbbom7n3gica9gqteokl39474etuib075x4lhig8dvocg32jwycjwfjzmzfh2ukqnemkxt6xlyq5ze8x7okzf66sgxrzep0m50yirndmhnu9t1ywaycup2k0j6be15l7amfyk29u14alvodnqk6644vt0oldwmm6p082rjyxatszf91qbmhbi1i4g
menuCode 会随着最大菜单 ID 增大而变得非常长,不过可以通过分组来解决,每个分组的菜单 ID 都是从 1 开始自增,并将分组 ID 写到编码前几位。
评论