權(quán)限之數(shù)據(jù)權(quán)限

概念

無論為數(shù)據(jù)操作賦予怎樣的業(yè)務(wù)含義,其本質(zhì)上仍然是數(shù)據(jù)的增刪改查操作(如下圖)。

image.png

隨著業(yè)務(wù)的演進,逐漸衍生出精細化管理數(shù)據(jù)的訴求。我遇到的業(yè)務(wù)場景是在企業(yè)級數(shù)據(jù)管理中,對不同職級的員工展示不同的數(shù)據(jù)。我的業(yè)務(wù)上的訴求是對SELECT進行權(quán)限控制,對INSERT、UPDATE、DELETE沒有權(quán)限限制要求。

數(shù)據(jù)權(quán)限實現(xiàn)的復(fù)雜度還是較高的,在敘述實現(xiàn)之前,我們先預(yù)設(shè)期望的結(jié)果: 能夠?qū)⒎爆嵉募毠?jié)都封裝在內(nèi)部邏輯中,對外部提供統(tǒng)一的接口調(diào)用。

相關(guān)設(shè)計理念可以參考我之前寫的文章,《外觀模式(封裝交互,簡化調(diào)用)》

在這個模型中,我們可選切入點有:

  • 用戶層面進行業(yè)務(wù)邏輯判斷(不推薦)
  • SQL層面上的抽象
  • 數(shù)據(jù)庫視圖(不推薦)

我在這里選擇了使用SQL來完成數(shù)據(jù)權(quán)限的實現(xiàn),通過SQL的組裝來完成寬泛的數(shù)據(jù)權(quán)限的控制。

原型實現(xiàn)

背景:某超市擁有員工 5 名,其組織架構(gòu)如下圖。該小超市的信息化程度極高,已經(jīng)擁有完備的移動版的CRM系統(tǒng)。

image.png

訴求:

  • 店長可以看到所有的銷售數(shù)據(jù);
  • 營業(yè)員可以看到自己的銷售數(shù)據(jù),但是不能看到別人的銷售數(shù)據(jù);
  • 收銀出納可以看到所有人的銷售數(shù)據(jù);
  • 采購庫管不能看到銷售數(shù)據(jù);

先貼上原型實現(xiàn),說明流程:

// 規(guī)則對象用于定義規(guī)則
// 這些規(guī)則包括用戶定義和管理員定義
// 規(guī)則應(yīng)可以序列化(此處省略)
public class RuleDataVO {

    // 受眾群體
    private String audienceGroup;
    // 規(guī)則實體
    private String rule;

    public RuleDataVO(String audienceGroup, String rule) {
        this.audienceGroup = audienceGroup;
        this.rule = rule;
    }

    public String getAudienceGroup() {
        return audienceGroup;
    }

    public void setAudienceGroup(String audienceGroup) {
        this.audienceGroup = audienceGroup;
    }

    public String getRule() {
        return rule;
    }

    public void setRule(String rule) {
        this.rule = rule;
    }
}
public class SaleDataVO {
    private Long userId;
    private Long storeId;
    private String goodName;
    private Integer goodPrice;

    public SaleDataVO(Long userId, Long storeId, String goodName, Integer goodPrice) {
        this.userId = userId;
        this.storeId = storeId;
        this.goodName = goodName;
        this.goodPrice = goodPrice;
    }

    public Long getUserId() {
        return userId;
    }

    public void setUserId(Long userId) {
        this.userId = userId;
    }

    public Long getStoreId() {
        return storeId;
    }

    public void setStoreId(Long storeId) {
        this.storeId = storeId;
    }

    public String getGoodName() {
        return goodName;
    }

    public void setGoodName(String goodName) {
        this.goodName = goodName;
    }

    public Integer getGoodPrice() {
        return goodPrice;
    }

    public void setGoodPrice(Integer goodPrice) {
        this.goodPrice = goodPrice;
    }

    @Override
    public String toString() {
        return goodName + ":" + goodPrice / 100.0;
    }
}

// 用于模擬數(shù)據(jù)庫操作
public class MockDataSource {

    // 模擬銷售數(shù)據(jù)庫表
    public static class SaleDataTable {
        private static List<SaleDataVO> list = new ArrayList<>();

        static {
            list.add(new SaleDataVO(1L, 1L, "貝因美奶粉", 9800));
            list.add(new SaleDataVO(1L, 1L, "毛巾", 2810));
            list.add(new SaleDataVO(2L, 1L, "黑人牙膏", 3200));
        }

        public static List<SaleDataVO> retrieval(Long userId, RuleDataVO ruleDataVO) {


            String ruleContent = ruleDataVO.getRule();
            if (ruleContent != null) {
                if (Objects.equals("自身", ruleContent)) {
                    // 如果檢查是營業(yè)員
                    if (userId == 1 || userId == 2) {
                        return filter(userId);
                    } else {
                        return filter(null);
                    }
                } else if (Objects.equals("本門店內(nèi)", ruleContent)) {
                    return filter(1L, 2L);
                } else if (Objects.equals("無", ruleContent)) {
                    return filter(null);
                }
            }
            return filter(null);
        }

        private static List<SaleDataVO> filter(Long... userIds) {
            List<SaleDataVO> saleDataVOS = new ArrayList<>();
            if (userIds == null) return saleDataVOS;
            for (SaleDataVO saleDataVO : list) {
                for (Long userId : userIds) {
                    if (saleDataVO.getUserId().equals(userId)) {
                        saleDataVOS.add(saleDataVO);
                        break;
                    }
                }
            }
            return saleDataVOS;
        }
    }

    // 模擬規(guī)則庫表
    public static class RuleTable {
        private static List<RuleDataVO> list = new ArrayList<>();

        static {

            list.add(new RuleDataVO("營業(yè)員", "自身"));
            list.add(new RuleDataVO("店長", "本門店內(nèi)"));
            list.add(new RuleDataVO("收銀出納", "本門店內(nèi)"));
            list.add(new RuleDataVO("采購庫管", "無"));
        }

        public static void add(RuleDataVO dataVO) {
            list.add(dataVO);
        }

        public static RuleDataVO retrieval(String audienceGroup) {
            for (RuleDataVO dataVO : list) {
                if (dataVO.getAudienceGroup().equalsIgnoreCase(audienceGroup)) {
                    return dataVO;
                }
            }
            return null;
        }
    }


}
public class Main {

    public static void main(String[] args) {
        System.out.println(MockDataSource.SaleDataTable.retrieval(1L, MockDataSource.RuleTable.retrieval("營業(yè)員")));
        System.out.println(MockDataSource.SaleDataTable.retrieval(2L, MockDataSource.RuleTable.retrieval("營業(yè)員")));
        System.out.println(MockDataSource.SaleDataTable.retrieval(3L, MockDataSource.RuleTable.retrieval("店長")));
        System.out.println(MockDataSource.SaleDataTable.retrieval(4L, MockDataSource.RuleTable.retrieval("收銀出納")));
        System.out.println(MockDataSource.SaleDataTable.retrieval(5L, MockDataSource.RuleTable.retrieval("采購庫管")));
    }
}

// 調(diào)用結(jié)果:
[貝因美奶粉:98.0, 毛巾:28.1]
[黑人牙膏:32.0]
[貝因美奶粉:98.0, 毛巾:28.1, 黑人牙膏:32.0]
[貝因美奶粉:98.0, 毛巾:28.1, 黑人牙膏:32.0]
[]

在這個原型上省略了不必要的復(fù)雜性(如DB操作,業(yè)務(wù)操作),僅關(guān)注規(guī)則的定義與解析過程。
原型上簡單定義了自身 本門店內(nèi) 的語法規(guī)則,結(jié)合上下文判拼接處正確的SQL語句。

我理解的權(quán)限控制核心就在這里:定義語法規(guī)則解析并應(yīng)用到SQL規(guī)范中。

  • 后端上定義語法規(guī)則,預(yù)初始化入庫,即完成數(shù)據(jù)權(quán)限的控制。
  • 前端上定義語法規(guī)則(需考慮SQL注入問題),即時操作入庫,即完成數(shù)據(jù)權(quán)限的控制;

上述是個非常簡單的原型,說明了解題思路但是實際的可操作性不高。因此我們需要接著對它進行抽象。

抽象

指令:查詢當天的銷售數(shù)據(jù);
環(huán)境:基于上下文參數(shù)推斷出所屬的資源,如:所屬的公司、部門等;
權(quán)限:僅自身相關(guān)的數(shù)據(jù)、本部門內(nèi)、本部門及下屬部門、所有、無;

對象 環(huán)境 權(quán)限 SQL
營業(yè)員 好又多超市101分店 僅自身相關(guān)的數(shù)據(jù) uid=$uid
收銀出納 好又多超市101分店 本部門及下屬部門 uid in $uids
采購庫管 好又多超市101分店 uid = null
店長 好又多超市101分店 本部門及下屬部門 uid in $uids

實現(xiàn)步驟拆分

  • 組織樹
  • 人與組織樹
  • 角色與功能權(quán)限
  • 角色與數(shù)據(jù)權(quán)限
  • 角色與人
  • 應(yīng)用權(quán)限規(guī)則

組織樹

image.png

通常業(yè)務(wù)限定組織樹的深度都不會過高,一般在5層以內(nèi)。實現(xiàn)組織樹的方式有多種:

定義組織樹目的是承載人的容器,通過將人分配到對應(yīng)組織中完成上下文的聯(lián)系。

image.png

通過將人分配到不同的部門中,即完成了人與組織的關(guān)系。這樣我們就能通過上下文推導(dǎo)出人所具備的資源。

通過對組織的資源限制即可完成預(yù)初始化狀態(tài)的SQL配置,如:

// 大老板
SELECT * FROM table;
// 李妲
 SELECT * FROM table WHERE department in ('行政部','銷售部');
// 李達
 SELECT * FROM table WHERE department = '行政部';
// 肖雨
 SELECT * FROM table WHERE store = '六盤水...;

解題步驟

* 添加組織(建設(shè)組織樹)
* 添加人
* 添加功能權(quán)限
* 分配角色到功能權(quán)限
* 添加數(shù)據(jù)權(quán)限
* 分配角色到數(shù)據(jù)權(quán)限
* 分配人到組織
* 分配人到角色

目前整個數(shù)據(jù)權(quán)限管理流程已經(jīng)做通了,具體的代碼涉及業(yè)務(wù)細節(jié)就不貼出來了。
如果有各種疑問,歡迎提問。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容