天天看點

一種适用于大量租戶大量角色的權限系統設計

作者:小小怪下士的架構攻略
一種适用于大量租戶大量角色的權限系統設計

前言

權限管理是每個系統不可缺少的一部分,大部分開發者應該都設計過權限管理系統,很多開發者學習的第一個項目可能就是權限管理系統。但是常見的權限設計在租戶量非常大、角色數量非常多時會存在角色權限表資料量指數增長的情況,本文介紹一種可以避免這種情況的權限設計思路。

傳統權限設計方案

傳統的權限系統設計一般有四張表分别為 菜單表、角色表、角色菜單表、使用者角色表,我們先按傳統權限系統設計一套資料表結構:

菜單表 SYS_MENU
ID NAME
1 賬務管理
2 使用者管理
3 訂單管理
角色 SYS_ROLE
ID NAME
1 管理者
2 普通使用者
角色菜單 SYS_ROLE_MENU
ID ROLE_ID MENU_ID
1 1 1
2 1 2
1 2 2
使用者角色 SYS_USER_ ROLE
ID ROLE_ID USER_ID
1 1 1
2 2 1
1 2 2

資料指數增長問題

如果我們系統有1萬個租戶,每個租戶有100個角色,每個角色有100個菜單點,則SYS_ROLE_MENU資料量為1億條資料,這個資料是非常恐怖的。

新的權限設計

角色表 SYS_ROLE
ID ROLE_NAME MENU_CODE
1 管理者 1fd4

對角色的菜單點進行編碼,我們先建構一個二進制,預設為全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長度會小很多。

java複制代碼   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 對象

java複制代碼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);
    }
}
           

建立角色表

java複制代碼@Data
@TableName("sys_role")
public class SysRole  {
    @TableId
    private Long id;
    private String roleName;
    private MenuCode menuCode;
}
           

給MenuCode建立TypeHandler

java複制代碼@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格式為:

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寫到編碼前幾位。

作者:趙俠客

連結:https://juejin.cn/post/7236680350557601847

繼續閱讀