Java利用Mybatis進(jìn)行數(shù)據(jù)權(quán)限控制

權(quán)限控制主要分為兩塊,認(rèn)證(Authentication)與授權(quán)(Authorization)设哗。認(rèn)證之后確認(rèn)了身份正確逮京,業(yè)務(wù)系統(tǒng)就會(huì)進(jìn)行授權(quán)翰舌,現(xiàn)在業(yè)界比較流行的模型就是RBAC(Role-Based Access Control)。RBAC包含為下面四個(gè)要素:用戶唤锉、角色世囊、權(quán)限、資源窿祥。用戶是源頭株憾,資源是目標(biāo),用戶綁定至角色晒衩,資源與權(quán)限關(guān)聯(lián)号胚,最終將角色與權(quán)限關(guān)聯(lián),就形成了比較完整靈活的權(quán)限控制模型浸遗。
資源是最終需要控制的標(biāo)的物猫胁,但是我們?cè)谝粋€(gè)業(yè)務(wù)系統(tǒng)中要將哪些元素作為待控制的資源呢?我將系統(tǒng)中待控制的資源分為三類:

  1. URL訪問(wèn)資源(接口以及網(wǎng)頁(yè))
  2. 界面元素資源(增刪改查導(dǎo)入導(dǎo)出的按鈕跛锌,重要的業(yè)務(wù)數(shù)據(jù)展示與否等)
  3. 數(shù)據(jù)資源

現(xiàn)在業(yè)內(nèi)普遍的實(shí)現(xiàn)方案實(shí)際上很粗放弃秆,就是單純的“菜單控制”,通過(guò)菜單顯示與否來(lái)達(dá)到控制權(quán)限的目的髓帽。
我仔細(xì)分析過(guò)菠赚,現(xiàn)在大家做的平臺(tái)分為To C和To B兩種:

  1. To C一般不會(huì)有太多的復(fù)雜權(quán)限控制,甚至大部分連菜單控制都不用郑藏,全部都可以訪問(wèn)衡查。
  2. To B一般都不是開(kāi)放的,只要做好認(rèn)證關(guān)口必盖,能夠進(jìn)入系統(tǒng)的只有內(nèi)部員工拌牲。大部分企業(yè)內(nèi)部的員工互聯(lián)網(wǎng)知識(shí)有限俱饿,而且作為內(nèi)部員工不敢對(duì)系統(tǒng)進(jìn)行破壞性的嘗試。

所以針對(duì)現(xiàn)在的情況塌忽,考慮成本與產(chǎn)出拍埠,大部分設(shè)計(jì)者也不愿意在權(quán)限上進(jìn)行太多的研發(fā)力量。
菜單和界面元素一般都是由前端編碼配合存儲(chǔ)數(shù)據(jù)實(shí)現(xiàn)土居,URL訪問(wèn)資源的控制也有一些框架比如SpringSecurity枣购,Shiro。
目前我還沒(méi)有找到過(guò)數(shù)據(jù)權(quán)限控制的框架或者方法擦耀,所以自己整理了一份棉圈。

數(shù)據(jù)權(quán)限控制原理

數(shù)據(jù)權(quán)限控制最終的效果是會(huì)要求在同一個(gè)數(shù)據(jù)請(qǐng)求方法中,根據(jù)不同的權(quán)限返回不同的數(shù)據(jù)集眷蜓,而且無(wú)需并且不能由研發(fā)編碼控制分瘾。這樣大家的第一想法應(yīng)該就是AOP,攔截所有的底層方法账磺,加入過(guò)濾條件芹敌。這樣的方式兼容性較強(qiáng),但是復(fù)雜程度也會(huì)更高垮抗。我們這套系統(tǒng)中氏捞,采用的是利用Mybatis的plugin機(jī)制,在底層SQL解析時(shí)替換增加過(guò)濾條件冒版。
這樣一套控制機(jī)制存在很明顯的優(yōu)缺點(diǎn)液茎,首先缺點(diǎn):

  1. 適用性有限,基于底層的Mybatis辞嗡。
  2. 方言有限捆等,針對(duì)了某種數(shù)據(jù)庫(kù)(我們使用Mysql),而且由于需要在底層解析處理?xiàng)l件所以有可能造成不同的數(shù)據(jù)庫(kù)不能兼容续室。當(dāng)然Redis和NoSQL也無(wú)法限制栋烤。

當(dāng)然,假如你現(xiàn)在就用Mybatis挺狰,而且數(shù)據(jù)庫(kù)使用的是Mysql明郭,這方面就沒(méi)有太大影響了。

接下來(lái)說(shuō)說(shuō)優(yōu)點(diǎn):

  1. 減少了接口數(shù)量及接口復(fù)雜度丰泊。原本針對(duì)不同的角色薯定,可能會(huì)區(qū)分不同的接口或者在接口實(shí)現(xiàn)時(shí)利用流程控制邏輯來(lái)區(qū)分不同的條件。有了數(shù)據(jù)權(quán)限控制瞳购,代碼中只用寫(xiě)基本邏輯话侄,權(quán)限過(guò)濾由底層機(jī)制自動(dòng)處理。
  2. 提高了數(shù)據(jù)權(quán)限控制的靈活性。例如原本只有主管能查本部門下組織架構(gòu)/訂單數(shù)據(jù)年堆,現(xiàn)在新增助理角色吞杭,能夠查詢本部門下組織架構(gòu),不能查詢訂單嘀韧。這樣的話普通的寫(xiě)法就需要調(diào)整邏輯控制篇亭,使用數(shù)據(jù)權(quán)限控制的話缠捌,直接修改配置就好锄贷。

數(shù)據(jù)權(quán)限實(shí)現(xiàn)

上一節(jié)就提及了實(shí)現(xiàn)原理,是基于Mybatis的plugins(查看官方文檔)實(shí)現(xiàn)曼月。

MyBatis 允許你在已映射語(yǔ)句執(zhí)行過(guò)程中的某一點(diǎn)進(jìn)行攔截調(diào)用谊却。默認(rèn)情況下,MyBatis 允許使用插件來(lái)攔截的方法調(diào)用包括:
Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
ParameterHandler (getParameterObject, setParameters)
ResultSetHandler (handleResultSets, handleOutputParameters)
StatementHandler (prepare, parameterize, batch, update, query)

Mybatis的插件機(jī)制目前比較出名的實(shí)現(xiàn)應(yīng)該就是PageHelper項(xiàng)目了哑芹,在做這個(gè)實(shí)現(xiàn)的時(shí)候也參考了PageHelper項(xiàng)目的實(shí)現(xiàn)方式炎辨。所以權(quán)限控制插件的類命名為PermissionHelper。
機(jī)制是依托于Mybatis的plugins機(jī)制聪姿,實(shí)際SQL處理的時(shí)候基于jsqlparser這個(gè)包碴萧。
設(shè)計(jì)中包含兩個(gè)類,一個(gè)是保存角色與權(quán)限的實(shí)體類命名為PermissionRule末购,一個(gè)是根據(jù)實(shí)體變更底層SQL語(yǔ)句的主體方法類PermissionHelper破喻。

首先來(lái)看下PermissionRule的結(jié)構(gòu):

public class PermissionRule {

    private static final Log log = LogFactory.getLog(PermissionRule.class);
    /**
     * codeName<br>
     * 適用角色列表<br>
     * 格式如: ,RoleA,RoleB,
     */
    private String roles;
    /**
     * codeValue<br>
     * 主實(shí)體,多表聯(lián)合
     * 格式如: ,SystemCode,User,
     */
    private String fromEntity;
    /**
     * codeDesc<br>
     * 過(guò)濾表達(dá)式字段, <br>
     * <code>{uid}</code>會(huì)自動(dòng)替換為當(dāng)前用戶的userId<br>
     * <code>{me}</code> main entity 主實(shí)體名稱
     * <code>{me.a}</code> main entity alias 主實(shí)體別名
     * 格式如:
     * <ul>
     * <li>userId = {uid}</li>
     * <li>(userId = {uid} AND authType > 3)</li>
     * <li>((userId = {uid} AND authType) > 3 OR (dept in (select dept from depts where manager.id = {uid})))</li>
     * </ul>
     */
    private String exps;

    /**
     * codeShowName<br>
     * 規(guī)則說(shuō)明
     */
    private String ruleComment;

}

看完這個(gè)結(jié)構(gòu)盟榴,基本能夠理解設(shè)計(jì)的思路了曹质。數(shù)據(jù)結(jié)構(gòu)中保存如下幾個(gè)字段:

  • 角色列表:需要使用此規(guī)則的角色,可以多個(gè)擎场,使用英文逗號(hào)隔開(kāi)羽德。
  • 實(shí)體列表:對(duì)應(yīng)的規(guī)則應(yīng)用的實(shí)體(這里指的是表結(jié)構(gòu)中的表名,可能你的實(shí)體是駝峰而數(shù)據(jù)庫(kù)是蛇形迅办,所以這里要放蛇形那個(gè))宅静,可以多個(gè),使用英文逗號(hào)隔開(kāi)站欺。
  • 表達(dá)式:表達(dá)式就是數(shù)據(jù)權(quán)限控制的核心了姨夹。簡(jiǎn)單的說(shuō)這里的表達(dá)式就是一段SQL語(yǔ)句,其中設(shè)置了一些可替換值镊绪,底層會(huì)用對(duì)應(yīng)運(yùn)行時(shí)的變量替換對(duì)應(yīng)內(nèi)容匀伏,從而達(dá)到增加條件的效果。
  • 規(guī)則說(shuō)明:?jiǎn)渭兊囊粋€(gè)說(shuō)明字段蝴韭。

核心流程
系統(tǒng)啟動(dòng)時(shí)够颠,首先從數(shù)據(jù)庫(kù)加載出所有的規(guī)則。底層利用插件機(jī)制來(lái)攔截所有的查詢語(yǔ)句榄鉴,進(jìn)入查詢攔截方法后履磨,首先根據(jù)當(dāng)前用戶的權(quán)限列表篩選出PermissionRule列表蛉抓,然后循環(huán)列表中的規(guī)則,對(duì)語(yǔ)句中符合實(shí)體列表的表進(jìn)行條件增加剃诅,最終生成處理后的SQL語(yǔ)句巷送,退出攔截器,Mybatis執(zhí)行處理后SQL并返回結(jié)果矛辕。

講完P(guān)ermissionRule笑跛,再來(lái)看看PermissionHelper,首先是頭:

@Intercepts({@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})
public class PermissionHelper implements Interceptor {
}

頭部只是標(biāo)準(zhǔn)的Mybatis攔截器寫(xiě)法聊品,注解中的Signature決定了你的代碼對(duì)哪些方法攔截飞蹂,update實(shí)際上針對(duì)修改(Update)、刪除(Delete)生效翻屈,query是對(duì)查詢(Select)生效陈哑。

下面給出針對(duì)Select注入查詢條件限制的完整代碼:


    private String processSelectSql(String sql, List<PermissionRule> rules, UserDefaultZimpl principal) {
        try {
            String replaceSql = null;
            Select select = (Select) CCJSqlParserUtil.parse(sql);
            PlainSelect selectBody = (PlainSelect) select.getSelectBody();
            String mainTable = null;
            if (selectBody.getFromItem() instanceof Table) {
                mainTable = ((Table) selectBody.getFromItem()).getName().replace("`", "");
            } else if (selectBody.getFromItem() instanceof SubSelect) {
                replaceSql = processSelectSql(((SubSelect) selectBody.getFromItem()).getSelectBody().toString(), rules, principal);
            }
            if (!ValidUtil.isEmpty(replaceSql)) {
                sql = sql.replace(((SubSelect) selectBody.getFromItem()).getSelectBody().toString(), replaceSql);
            }
            String mainTableAlias = mainTable;
            try {
                mainTableAlias = selectBody.getFromItem().getAlias().getName();
            } catch (Exception e) {
                log.debug("當(dāng)前sql中, " + mainTable + " 沒(méi)有設(shè)置別名");
            }


            String condExpr = null;
            PermissionRule realRuls = null;
            for (PermissionRule rule :
                    rules) {
                for (Object roleStr :
                        principal.getRoles()) {
                    if (rule.getRoles().indexOf("," + roleStr + ",") != -1) {
                        if (rule.getFromEntity().indexOf("," + mainTable + ",") != -1) {
                            // 若主表匹配規(guī)則主體伸眶,則直接使用本規(guī)則
                            realRuls = rule;

                            condExpr = rule.getExps().replace("{uid}", UserDefaultUtil.getUserId().toString()).replace("{bid}", UserDefaultUtil.getBusinessId().toString()).replace("{me}", mainTable).replace("{me.a}", mainTableAlias);
                            if (selectBody.getWhere() == null) {
                                selectBody.setWhere(CCJSqlParserUtil.parseCondExpression(condExpr));
                            } else {
                                AndExpression and = new AndExpression(selectBody.getWhere(), CCJSqlParserUtil.parseCondExpression(condExpr));
                                selectBody.setWhere(and);
                            }
                        }

                        try {
                            String joinTable = null;
                            String joinTableAlias = null;
                            for (Join j :
                                    selectBody.getJoins()) {
                                if (rule.getFromEntity().indexOf("," + ((Table) j.getRightItem()).getName() + ",") != -1) {
                                    // 當(dāng)主表不能匹配時(shí)惊窖,匹配所有join,使用符合條件的第一個(gè)表的規(guī)則厘贼。
                                    realRuls = rule;
                                    joinTable = ((Table) j.getRightItem()).getName();
                                    joinTableAlias = j.getRightItem().getAlias().getName();

                                    condExpr = rule.getExps().replace("{uid}", UserDefaultUtil.getUserId().toString()).replace("{bid}", UserDefaultUtil.getBusinessId().toString()).replace("{me}", joinTable).replace("{me.a}", joinTableAlias);
                                    if (j.getOnExpression() == null) {
                                        j.setOnExpression(CCJSqlParserUtil.parseCondExpression(condExpr));
                                    } else {
                                        AndExpression and = new AndExpression(j.getOnExpression(), CCJSqlParserUtil.parseCondExpression(condExpr));
                                        j.setOnExpression(and);
                                    }
                                }
                            }
                        } catch (Exception e) {
                            log.debug("當(dāng)前sql沒(méi)有join的部分界酒!");
                        }
                    }
                }
            }
            if (realRuls == null) return sql; // 沒(méi)有合適規(guī)則直接退出。

            if (sql.indexOf("limit ?,?") != -1 && select.toString().indexOf("LIMIT ? OFFSET ?") != -1) {
                sql = select.toString().replace("LIMIT ? OFFSET ?", "limit ?,?");
            } else {
                sql = select.toString();
            }

        } catch (JSQLParserException e) {
            log.error("change sql error .", e);
        }
        return sql;
    }

重點(diǎn)思路
重點(diǎn)其實(shí)就在于Sql的解析和條件注入涂臣,使用開(kāi)源項(xiàng)目JSqlParser盾计。

  • 解析出MainTable和JoinTable。from之后跟著的稱為MainTable赁遗,join之后跟著的稱為JoinTable署辉。這兩個(gè)就是我們PermissionRule需要匹配的表名,PermissionRule::fromEntity字段岩四。
  • 解析出MainTable的where和JoinTable的on后面的條件哭尝。使用and連接原本的條件和待注入的條件,PermissionRule::exps字段剖煌。
  • 使用當(dāng)前登錄的用戶信息(放在緩存中)材鹦,替換條件表達(dá)式中的值。
  • 某些情況需要忽略權(quán)限耕姊,可以考慮使用ThreadLocal(單機(jī))/Redis(集群)來(lái)控制桶唐。

結(jié)束語(yǔ)

想要達(dá)到無(wú)感知的數(shù)據(jù)權(quán)限控制,只有機(jī)制控制這么一條路茉兰。本文選擇的是通過(guò)底層攔截Sql語(yǔ)句尤泽,并且針對(duì)對(duì)應(yīng)表注入條件語(yǔ)句這么一種做法。應(yīng)該是非常經(jīng)濟(jì)的做法,只是基于文本處理坯约,不會(huì)給系統(tǒng)帶來(lái)太大的負(fù)擔(dān)熊咽,而且能夠達(dá)到理想中的效果。大家也可以提出其他的見(jiàn)解和思路闹丐。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末横殴,一起剝皮案震驚了整個(gè)濱河市视粮,隨后出現(xiàn)的幾起案子绳瘟,更是在濱河造成了極大的恐慌硼身,老刑警劉巖乌助,帶你破解...
    沈念sama閱讀 218,755評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異含滴,居然都是意外死亡辐棒,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門航徙,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人陷虎,你說(shuō)我怎么就攤上這事到踏。” “怎么了尚猿?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,138評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵窝稿,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我凿掂,道長(zhǎng)伴榔,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,791評(píng)論 1 295
  • 正文 為了忘掉前任庄萎,我火速辦了婚禮踪少,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘糠涛。我一直安慰自己援奢,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布忍捡。 她就那樣靜靜地躺著集漾,像睡著了一般。 火紅的嫁衣襯著肌膚如雪砸脊。 梳的紋絲不亂的頭發(fā)上具篇,一...
    開(kāi)封第一講書(shū)人閱讀 51,631評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音凌埂,去河邊找鬼驱显。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的秒紧。 我是一名探鬼主播绢陌,決...
    沈念sama閱讀 40,362評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼熔恢!你這毒婦竟也來(lái)了脐湾?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,264評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤叙淌,失蹤者是張志新(化名)和其女友劉穎秤掌,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體鹰霍,經(jīng)...
    沈念sama閱讀 45,724評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡闻鉴,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了茂洒。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片孟岛。...
    茶點(diǎn)故事閱讀 40,040評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖督勺,靈堂內(nèi)的尸體忽然破棺而出渠羞,到底是詐尸還是另有隱情,我是刑警寧澤智哀,帶...
    沈念sama閱讀 35,742評(píng)論 5 346
  • 正文 年R本政府宣布次询,位于F島的核電站,受9級(jí)特大地震影響瓷叫,放射性物質(zhì)發(fā)生泄漏屯吊。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評(píng)論 3 330
  • 文/蒙蒙 一摹菠、第九天 我趴在偏房一處隱蔽的房頂上張望盒卸。 院中可真熱鬧,春花似錦辨嗽、人聲如沸世落。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,944評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)屉佳。三九已至,卻和暖如春洲押,著一層夾襖步出監(jiān)牢的瞬間武花,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,060評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工杈帐, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留体箕,地道東北人专钉。 一個(gè)月前我還...
    沈念sama閱讀 48,247評(píng)論 3 371
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像累铅,于是被迫代替她去往敵國(guó)和親跃须。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • 1. 簡(jiǎn)介 1.1 什么是 MyBatis 娃兽? MyBatis 是支持定制化 SQL菇民、存儲(chǔ)過(guò)程以及高級(jí)映射的優(yōu)秀的...
    笨鳥(niǎo)慢飛閱讀 5,523評(píng)論 0 4
  • 1.JVM 堆內(nèi)存和非堆內(nèi)存 堆和非堆內(nèi)存按照官方的說(shuō)法:“Java 虛擬機(jī)具有一個(gè)堆(Heap),堆是運(yùn)行時(shí)數(shù)據(jù)...
    yanzhu728閱讀 915評(píng)論 0 0
  • 這部分主要是開(kāi)源Java EE框架方面的內(nèi)容投储,包括Hibernate第练、MyBatis、Spring玛荞、Spring ...
    雜貨鋪老板閱讀 1,385評(píng)論 0 2
  • MyBatis 理論篇 [TOC] 什么是MyBatis ?MyBatis是支持普通SQL查詢,存儲(chǔ)過(guò)程和高級(jí)映射...
    有_味閱讀 2,904評(píng)論 0 26
  • 一段時(shí)間走過(guò)娇掏,十年,二十年勋眯,從開(kāi)始到如今婴梧。 像是一路走過(guò),風(fēng)景凡恍,另一個(gè)風(fēng)景志秃,從萬(wàn)般皆無(wú)到一路遇見(jiàn)。 一場(chǎng)繁花開(kāi)謝嚼酝,...
    ly12386閱讀 277評(píng)論 1 3