數據權限就該這麼實現(實現篇)

語言: CN / TW / HK

大家好,在上一篇文章中我們詳細介紹了在RBAC模型中如何集成數據權限,本篇文章我們將通過實際案例,從代碼實戰的角度來實現這樣的一個數據權限。

在開始閲讀本文之前,建議先把上篇文章 讀一遍,讀一遍,讀一遍!
RBAC權限設計中如何整合數據權限?

數據權限模型

上篇文章的數據模型是基於傳統的RBAC模型來設計的,由於我們這裏的應用場景不一樣,所以這裏的數據權限模型並沒有嚴格按照上篇文章的方案來設計,但是萬變不離其宗,核心原理還是相同的。

首先我來介紹一下我們最終實現的效果

實現效果

image-20220802201847624

一個組件(可以理解成菜單)可以綁定多個授權維度,當給角色授權組件時可以給這個授權組件賦予不同維度的權限。

關於數據權限的授權維度有以下幾個關鍵點需要仔細體會:

  1. 給一個角色勾選授權維度實際上是在限制這個角色所能看到的數據範圍

  2. 任何一個授權維度勾選了"全部",相當於不限制此維度的權限。

如果一個角色勾選了客户羣的全部 + A產品線,那麼最終生成的sql 會是 where 產品線 in ('A產品線')

  1. 如果一個角色勾選了多個維度,維度之間用 AND 拼接

​ 如果一個角色勾選了A客户羣 + B產品線,那麼最終生成的sql 會是 where 客户羣 in('A客户羣')AND 產品線 in ('B產品線')

  1. 一個用户可能有多個角色,角色之間用 OR 拼接

​ 一個用户有兩個角色:客户羣總監,產品線經理。其中客户羣總監角色擁有A客户羣和B客户羣的權限,產品線經理角色擁有A產品線權限,那麼最終生成的sql會是 where 客户羣 in ('A客户羣','B客户羣') OR 產品線 in ('A產品線')

當然我們業務場景中數據規則比較單一,全部使用 in作為sql條件連接符,你們可以根據實際業務場景進行補充。

數據模型

最終的數據模型如下所示:

image-20220803172324998

這裏的組件大家完全可以理解成RBAC模型中的資源、菜單,只不過叫法不同而已。

數據權限表結構

下面是具體的表結構設計

授權維度表

sql CREATE TABLE `wb_dimension` ( `ID` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '主鍵', `DIMENSION_CODE` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '維度編碼', `DIMENSION_NAME` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '維度名稱', PRIMARY KEY (`ID`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC COMMENT='授權維度'

具體授權維度表(產品線)

sql CREATE TABLE `wb_dimension_proc_line` ( `ID` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '主鍵', `DIMENSION_CODE` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '維度編碼', `PROC_LINE_CODE` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '產品線編碼', `PROC_LINE_NAME` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '產品線名稱', PRIMARY KEY (`ID`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC COMMENT='授權維度-產品線'

跟授權維度表實際是一個表繼承的關係,由於每個授權維度的屬性不一樣,展現形式也不一樣,所以分表存儲。

組件路由表

sql CREATE TABLE `wb_route` ( `ID` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '主鍵ID', `COMPONENT_ID` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '組件ID', `ROUTE_URL` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '路由地址', `AUTHORIZATION_TYPE` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '授權方式:1 自定義,2 上下級授權, 3 範圍授權', `AUTHORIZATION_DIMENSION` json DEFAULT NULL COMMENT '授權維度', PRIMARY KEY (`ID`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC COMMENT='組件路由'

當組件屬性授權方式為範圍授權時在應用側會強制要求選擇具體的授權維度,如 產品線、客户羣。

角色表

sql CREATE TABLE `wb_role` ( `ID` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '主鍵ID', `ROLE_CODE` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '角色CODE', `ROLE_NAME` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '角色名稱', `IDENTITY_ID` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '身份ID' PRIMARY KEY (`ID`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC COMMENT='角色表'

角色上有一個身份屬性,多個角色可以歸屬同一個身份,方便對角色進行分類管理。

角色組件綁定表

sql CREATE TABLE `role_component_relation` ( `ID` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '主鍵ID', `ROLE_ID` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '角色ID', `COMPONENT_ID` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '組件ID', PRIMARY KEY (`ID`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC COMMENT='角色授權組件'

角色組件授權規則表(核心)

sql CREATE TABLE `wb_role_component_rule` ( `ID` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '主鍵', `ROLE_ID` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '角色ID', `COMPONENT_ID` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '組件ID', `RULE_CODE` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '規則編碼', `RULE_NAME` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '規則名稱', `RULE_CONDITION` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '規則條件', `RULE_VALUE` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '規則值', PRIMARY KEY (`ID`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC COMMENT='角色組件維度規則表'

數據權限的核心表,規則條件的取值為IN,規則值存儲具體的維度編碼,當在數據維度中選擇 全部 時我們將規則值存儲為ALL這個特殊值,方便後續生成SQL語句。

image-20220803232053689

實現過程

上篇文章中提到過數據權限的實現過程,這裏再回顧一下

  1. 自定義一個數據權限的註解,比如叫DataPermission
  2. 在對應的資源請求方法,比如商機列表上添加自定義註解@DataPermission
  3. 利用AOP抓取到用户對應角色的所有數據規則並進行SQL拼接,最終在SQL層面實現數據過濾。

代碼實現

自定義數據權限註解

```java @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE,ElementType.METHOD}) @Documented public @interface DataPermission { /* * 數據權限類型 * 1 上下級授權 2 數據範圍授權 / String permissionType() default "2";

/**
 * 配置菜單的組件路徑,用於數據權限
 */
String componentRoute() default "";

} ```

定義數據權限處理切面

```java @Aspect @Slf4j public class DataPermissionAspect {

@Autowired
private RoleComponentRuleService roleComponentRuleService;

@Pointcut("@annotation(com.ifly.workbench.security.annotation.DataPermission)")
public void pointCut() {

}

@Around("pointCut()")
public Object around(ProceedingJoinPoint point) throws  Throwable{

    HttpServletRequest request = SpringContextUtils.getHttpServletRequest();

    //獲取請求token
    String token = request.getHeader(CommonConstant.X_ACCESS_TOKEN);
    String userName = JwtUtil.getUsername(token);

    MethodSignature signature = (MethodSignature) point.getSignature();
    Method method = signature.getMethod();
    DataPermission permissionData = method.getAnnotation(DataPermission.class);

//獲取授權方式
    String permissionType = permissionData.permissionType();
    //獲取組件路由
    String componentRoute = permissionData.componentRoute();

if (StringUtils.isNotEmpty(componentRoute)){
        // 查找當前用户此組件下的所有規則
        List<RoleComponentRuleDTO> componentRules = roleComponentRuleService.getRoleComponentRule(userName, componentRoute);

        if(CollectionUtils.isNotEmpty(componentRules)){
                DataPermissionUtils.installDataSearchConditon(request, componentRules);
                SysUserCacheInfo userInfo = buildCacheUser(userName);
                DataPermissionUtils.installUserInfo(request, userInfo);
            }
    }

return  point.proceed();
}

private SysUserCacheInfo buildCacheUser(String userName) {
    SysUserCacheInfo info = new SysUserCacheInfo();
    info.setSysUserName(userName);
    info.setOneDepart(true);
    return info;
}

} ```

在AOP中獲取當前用户、需要訪問的組件中所有的數據規則,參考wb_role_component_rule表設計,並將其放到Request作用域中。

數據權限工具類

```java public class DataPermissionUtils {

public static final String COMPONENT_DATA_RULES = "COMPONENT_DATA_RULES";

public static final String SYS_USER_INFO = "SYS_USER_INFO";

/* * 往鏈接請求裏面,傳入數據查詢條件 * @param request * @param componentRules / public static void installDataSearchConditon(HttpServletRequest request, List componentRules) { // 1.先從request獲取MENU_DATA_AUTHOR_RULES,如果存則獲取到LIST List list = loadDataSearchCondition();

    if (list==null) {
        // 2.如果不存在,則new一個list
        list = Lists.newArrayList();
    }
    list.addAll(componentRules);
    // 3.往list裏面增量存指
    request.setAttribute(COMPONENT_DATA_RULES, list);
}

/**
 * 獲取請求對應的數據權限規則
 *
 */
@SuppressWarnings("unchecked")
public synchronized List<RoleComponentRuleDTO> loadDataSearchCondition() {
    return (List<RoleComponentRuleDTO>) SpringContextUtils.getHttpServletRequest().getAttribute(COMPONENT_DATA_RULES);

}

public synchronized void installUserInfo(HttpServletRequest request, SysUserCacheInfo userinfo) {
    request.setAttribute(SYS_USER_INFO, userinfo);
}

} ```

在Request中存儲數據規則。

查詢組件規則

```sql public interface RoleComponentRuleService extends IService {

/**
 * 根據 用户域賬户和組件編碼 獲取組件對應的關係
 *
 * @param userName      域賬號
 * @param componentCode 組件編碼
 * @return 用户的所有規則
 */
List<RoleComponentRuleDTO> getRoleComponentRule(String userName, String componentCode);

} ```

```sql @Service public class RoleComponentRuleServiceImpl extends ServiceImpl implements RoleComponentRuleService {

@Resource
private RoleComponentRuleMapper roleComponentRuleMapper;

/**
 * 根據 用户域賬户和組件編碼 獲取組件對應的關係
 * @param userName      域賬號
 * @param componentCode 組件編碼
 * @return 用户的所有規則
 */
@Override
public List<RoleComponentRuleDTO> getRoleComponentRule(String userName, String componentCode) {
    return roleComponentRuleMapper.getRoleComponentRule(userName,componentCode);
}

} ```

sql <select id="getRoleComponentRule" resultType="com.ifly.vo.RoleComponentRuleDTO"> SELECT tab1.id, tab1.role_id, tab4.role_code, tab1.component_id, tab1.rule_code, tab1.rule_name, tab1.rule_condition, tab1.rule_value, tab4.identity_id FROM wb_role_component_rule tab1 LEFT JOIN user_role_relation tab2 ON tab2.role_id = tab1.role_id LEFT JOIN wb_component tab3 ON tab3.id = tab1.component_id LEFT JOIN wb_role tab4 ON tab4.id = tab1.role_id JOIN role_component_relation tab5 ON tab5.role_id = tab1.role_id AND tab5.component_id = tab1.component_id where tab2.user_account = #{userName} and tab3.component_code = #{componentCode} </select>

Controller調用

```sql @ApiOperation(value = "服務BU-領導-總覽") @GetMapping("opp/getLeaderOverviewSve") @DataPermission(componentRoute = "020202") public Result getLeaderOverviewSve(@RequestParam(name = "identityId") String identityId) { String permissionSql = RuleQueryGenerator.getPermissionSql(identityId); log.info("查服務BU-領導-總覽-permissionSQL==" + permissionSql);

return Result.OK(overviewSveService.getLeaderOverviewSve(permissionSql));

} ```

在controller的請求方法上加上自定義註解@DataPermission並指定組件編碼,然後通過工具類生成SQL條件,最後將SQL條件傳入service層進行處理。

構建數據權限SQL

```java @Slf4j @UtilityClass public class RuleQueryGenerator {

private static final String SQL_AND = " and ";

private static final String SQL_OR = " or ";

private static final String SQL_JOINT = " (%s) ";

/**
 * 獲取帶有數據權限的SQL
 * @param identityId 身份ID
 */
public String getPermissionSql(String identityId) {
    //------------------------獲取當前身份的數據規則------------------------------------
    List<RoleComponentRuleDTO> conditionList = getCurrentIdentyPermission(identityId);
    if (CollectionUtils.isEmpty(conditionList)) {
        //沒有權限
        return "1 = 0";
    }
    //存在權限
    //對當前身份根據規則編碼分組-去除不同角色中相同編碼且規則值為ALL的規則 並根據角色id分組
    Map<String, List<RoleComponentRuleDTO>> ruleMap = getRuleMapByRoleId(conditionList);

    StringBuilder sb = new StringBuilder();
    String roleSql;
    if (MapUtils.isNotEmpty(ruleMap)) {
        //按角色拼接SQL
        for (Map.Entry<String, List<RoleComponentRuleDTO>> entry : ruleMap.entrySet()) {

            List<RoleComponentRuleDTO> lists = entry.getValue();

            // 同角色之間使用 AND
            roleSql = buildRoleSql(lists);

            //角色之間使用 OR
            if (StringUtils.isNotEmpty(roleSql)) {
                jointSqlByRoles(sb, roleSql);
            }
        }

    }
    return sb.toString();
}

private static List<RoleComponentRuleDTO> getCurrentIdentyPermission(String identityId) {
    //----------------------------獲取所有數據規則-----------------------------
    List<RoleComponentRuleDTO> roleRuleList = DataPermissionUtils.loadDataSearchCondition();
    if(CollectionUtils.isEmpty(roleRuleList)){
        return null;
    }
    //-----------------------------過濾掉不屬於當前身份的規則-----------------------------------
    return roleRuleList.stream()
            .filter(item -> item.getIdentityId().equals(identityId))
            .collect(Collectors.toList());
}

/**
 * 構建單角色SQL
 */
private static String buildRoleSql(List<RoleComponentRuleDTO> lists) {
    StringBuilder roleSql = new StringBuilder();
    for (RoleComponentRuleDTO item : lists) {
        //如果出現全選 則 代表全部,不需要限定範圍
        if ("ALL".equals(item.getRuleValue())) {
            continue;
        }
        //將規則轉換成SQL
        String filedSql = convertRuleToSql(item);

        roleSql.append(SQL_AND).append(filedSql);
    }
    return roleSql.toString();
}


/**
 * 將單一規則轉化成SQL,默認全部使用 In
 * ruleCode : area_test
 * ruleValue : 區域1,區域2,區域3
 * @param rule 規則值
 */
private static String convertRuleToSql(RoleComponentRuleDTO rule) {
    String whereCondition = " in ";
    String ruleValueConvert = getInConditionValue(rule.getRuleValue());
    return rule.getRuleCode() + whereCondition + ruleValueConvert;
}


/**
 * IN字符串轉換
 * 區域1, 區域2, 區域3  --> ("區域1","區域2","區域3")
 * 江西大區  --> ("江西大區")
 */
private static String getInConditionValue(String ruleValue) {
    String[] temp = ruleValue.split(",");
    StringBuilder res = new StringBuilder();
    for (String string : temp) {
        res.append(",'").append(string).append("'");
    }
    return "(" + res.substring(1) + ")";
}

/**
 * 拼接單角色的SQL
 * @param sqlBuilder 總的SQL
 * @param roleSql    單角色SQL
 */
private static void jointSqlByRoles(StringBuilder sqlBuilder, String roleSql) {
    roleSql = roleSql.replaceFirst(SQL_AND, "");
    if (StringUtils.isEmpty(sqlBuilder.toString())) {
        sqlBuilder.append(String.format(SQL_JOINT, roleSql));
    } else {
        sqlBuilder.append(SQL_OR).append(String.format(SQL_JOINT, roleSql));
    }
}

/**
 *
 * 1. 對當前身份根據規則編碼分組-去除不同角色中相同編碼且規則值為ALL的規則
 * 2. 對角色進行分組
 * @param conditionList 數據規則
 * @return 分組後的規則list
 */
private static Map<String, List<RoleComponentRuleDTO>> getRuleMapByRoleId(List<RoleComponentRuleDTO> conditionList) {
//--------過濾掉不屬於當前身份的規則,並對條件編碼進行分組-----------------------------------
Map<String, List<RoleComponentRuleDTO>> conditionMap = conditionList.stream().collect(Collectors.groupingBy(RoleComponentRuleDTO::getRuleCode));

    //--------相同編碼分組中存在ALL的排除掉-----------------------------------------------
List<RoleComponentRuleDTO> newRoleRuleList = new ArrayList<>();
if (MapUtils.isNotEmpty(conditionMap)) {
    for (Map.Entry<String, List<RoleComponentRuleDTO>> entry : conditionMap.entrySet()) {
    boolean flag = true;
    List<RoleComponentRuleDTO> lists = entry.getValue();
    for (RoleComponentRuleDTO item : lists) {
        if ("ALL".equals(item.getRuleValue())) {
        flag = false;
        break;
      }
    }

    if (flag) {
       newRoleRuleList.addAll(lists);
    }
    }
 }
 if (CollectionUtils.isNotEmpty(newRoleRuleList)) {
    return newRoleRuleList.stream().collect(Collectors.groupingBy(RoleComponentRuleDTO::getRoleId));
  }
 return Maps.newHashMap();
}

} ```

核心類,用於生成數據權限查詢的SQL腳本。

Dao層實現

xml <select id="getLeaderOverviewSve" resultType="com.ifly.center.entity.SalesProjOverviewSve"> SELECT <include refid="column_list"/> FROM U_STD_ADS.LTC_SALES_PROJ_OVERVIEW_SVE <where> <if test="permissionSql != null and permissionSql != ''"> ${permissionSql} </if> </where> </select>

Dao層接受service層傳入已經生成好的sql語句,作為查詢條件直接拼接在業務語句之後。

小結

以上,就是數據權限的實現過程,其實代碼實現並不複雜,主要還是得理解其中的實現原理。如果你也有數據權限的需求,不妨參考一下。當然如果你有更好的實現方案,也可以留言告訴我。