1. 程式人生 > >node實現後臺許可權管理系統

node實現後臺許可權管理系統

本文面向的是node初學者,目標是搭建一個基礎的後臺許可權系統。使用的node框架是上手最簡單的express,模板是ejs,這些在node入門的書籍中都有介紹說明,所以應該是難度較低的。

對於node初學者來說,可以先嚐試搭建一個blog,單使用者的或者多使用者的都可以。cnodejs論壇是我學的第一個原始碼,還是很經典的。但是由於開發比較早,基於nodev4版本,很多後面新加的特性都未使用,比如class,async/await語法等。隨著node版本的大幅升級,目前至少是基於nodev8,穩定的是nodev10版本開發。所以本系列教程node版本至少是8版本,推薦裝LTS 10版本。

ide強烈推薦速度最快,使用最流暢的Visual Studio Code。它也是基於node的electron實現的,一級棒。

資料庫用過MS SQLServer、MySQL、mongodb,其中mongodb好多node教程推薦使用才開始流行,但它並非關係型資料庫,所以綜合考慮還是選用MySQL。資料庫客戶端推薦Navicat Premium。

以上各環境的安裝準備工作大家可以自行教程,不在展開說明了。

說下此專案的目標是搭建一個後臺許可權管理系統。對於實際專案的業務開發,後臺的基礎許可權框架必不可少。本教程將會帶你如何設計使用者、角色、選單、及許可權控制,並通過程式碼示例實現。

如何定義一個好的框架,沒有一個標準。主要看每個人的水平及專案的適用性。以前從事.net開發,學設計模式、封裝、Ioc注入等,好固然是好,但也提高了入門的門檻,想要讓一個初學者快速上手進行業務開發卻很難。因為封裝後的程式碼可讀性變差,如果沒有一定水平框架也很難維護。還有就是如果一個專案比較小,那麼在設計框架時分層分個5、6層就有點過頭了,一般3層MVC就夠用了。當然如果你在大型網際網路公司,接觸到的使用者數量是幾萬甚至幾十萬,業務也很多,那框架就勢必要考慮到很多方面的問題,那架構設計就不是那麼簡簡單單的了,可能會涉及到分散式、微服務等。

我們可以先從基礎的簡單的框架入手,等經驗豐富了後再不斷的重構升級以應對日益增長的業務需求。

下面我們來開始設計並搭建框架。

最基礎的許可權是使用者的登入,通過使用者名稱和密碼跟資料庫匹配來判斷是否登入成功。

使用者登入並存入session後,下次只需判斷session中使用者是否存在,可以寫在express中介軟體中。

/** 許可權判斷中介軟體*/
class authMiddleware {
    /** 需要使用者登入*/
    async loginRequired(req, res, next) {
        if (!req.session || !req.session.user || !req.session.user.id) {
            return res.redirect('/login');
        }
        await next();
    }
}

module.exports = new authMiddleware();

然後在每次請求的路由中,先判斷下使用者是否已登入,然後再執行相應controller。

const router = require('express').Router(),
    auth = require('./middleware/auth'),
    login = require('./controller/login'),
    main = require('./controller/main');

router.get('/login', login.showLogin);
router.post('/login', login.login);

router.get('/main', auth.loginRequired, main.showMain);

其中登入頁面及登入post提交是不需要檢查session中有無使用者的,因為使用者這時候還沒登入成功。但是像主頁/main設定的是需要使用者登入才能檢視的。

登入後的後臺管理系統佈局一般都是左側是選單樹,右側為內容。

很顯然左側的選單需要許可權控制,用於區分哪些選單(頁面)使用者可以訪問,哪些選單(頁面)使用者不可以訪問。一般不能訪問的選單(頁面)不顯示給使用者,這樣不同的使用者登入後顯示的左側選單是不相同的。

可以從圖上看到,在原來的登入的基礎上增加了選單表和使用者選單表,一個使用者可以對應有多個選單。那麼他在登入後就獲得了相應的選單集合,在頁面左側載入即可。

對於那些在介面上沒顯示的選單在後臺路由中也是要檢查下許可權的,要不然像使用者管理/userList這條路由雖然沒有配給某個測試賬號,但他可以直接在位址列輸入/userList進行訪問。所以在Middleware中需要增加判斷使用者是否有此page_url的訪問許可權。

/** 需要使用者選單許可權*/
    async userPermission(req, res, next) {
       //先判斷使用者session
        if (!req.session || !req.session.user || !req.session.user.id) {
            return res.redirect('/login');
        }
        let hasPower = false;
        //userMenu指當前使用者擁有的選單集合,請自行db查詢
        userMenu.forEach(el => {
            if (el.page_url == req.route.path) {
                hasPower = true;
            }
        });
       if (!hasPower) {
            if (req.xhr) {
                return res.json({
                    state: false,
                    msg: "抱歉,您無此許可權!請聯絡管理員"
                });
            }
            return res.send('抱歉,您無此許可權!請聯絡管理員');
        }
        next();
    }

路由中增加中介軟體的執行

router.get('/userList', auth.userPermission, main.showUser);

這樣當瀏覽器請求/userList路由時,先執行中介軟體userPermission方法,判斷使用者是否擁有此url許可權,如果hasPower為false,即沒有許可權,直接返回輸出。如果有許可權,再執行相應controller中方法。

至此,基本的使用者登入,及使用者選單許可權已設計完成。但如果需要進一步許可權控制到頁面中的按鈕、對使用者進行分組設定許可權等,還得再進行補充完善。

按鈕(控制元件)

上面許可權系統控制到了頁面,如果頁面中不同使用者操作按鈕(控制元件)許可權需要區分,比如常見的增、刪、改操作,還需要進一步許可權設計。

頁面列表資料檢視、新增、修改、刪除等各種操作,都需要通過ajax將資料或引數提交給對應的介面,然後介面再將結果返回,所以我們可以通過限制介面的訪問來做到對頁面按鍵功能的控制。

可以把各種按鈕也看成是選單項,對前面的選單表進行擴充,增加(控制元件地址、是否顯示)兩個欄位即可。

其中控制元件地址這個欄位,就是我們訪問介面資料的路由地址,比如說獲取單個訂單資料是通過指定主鍵id,它的路由介面一般設計成'/api/user/:id'。這些路由地址是我們自己設計且在整個專案中都是唯一的。所以在開發時,我們可以在後臺做箇中間件攔截,通過使用者是否有這個選單項對應的介面路由地址,來判斷使用者是否有訪問該介面的許可權。

是否顯示欄位,是為了在輸出選單列表時將這些按鈕選單項隱藏,不在介面中顯示出來。

角色(職位)

當系統中使用者增多,對每一個使用者都需要單獨設定許可權這種重複勞動工作量增大的時候,勢必要考慮使用者組了,在我們windows系統中早就有使用者組的概念了。有了使用者組概念,就可以先設定好某個使用者組的許可權,然後對於新加進來的使用者只需加入之前配置好的使用者組中,即新加進來的使用者就擁有了使用者組的許可權。

考慮到現實情況,一個公司或企業的組織架構通常是有很多個部門組成,部門下面會有相應的職位,比如部門經理、部門員工等。用職位代替角色或使用者組更讓人能夠容易理解。不同的職位有不同的工作職責,也有不同的操作許可權。而部門主要是為了方便對職位進行分組管理,如果職位多時沒有對應的分組,查詢不方便,如果有相似的職位,也容易混淆。

對於企業來說,人員由於流動關係可能會經常變化,而職位則相對來說是比較固定的,所以許可權繫結職位更合適,而不許可權直接繫結使用者。當一位員工更換崗位時,只需要更改他所繫結的職位,對於新入職的員工,也只需要繫結他所入職崗位,他們就可以擁有該職位的所有許可權。

當然也會存在一些特殊的需要,比如說某人與同事都隸屬於同一個職位,但他是老員工可以擁有更多的許可權,這時可以增加一個新職位(通過制定職位級別)來區分他們的許可權。

又比如說,如果許可權需要限制部門訪問許可權,而該部門內的職位只能設定當前部門對應的許可權,如果有員工需要跨部門擁有其他許可權時,可以通過更改使用者賬號繫結多職位的方式來實現,也就是說一個員工他只可以繫結一個主部門,但他可以同時擁有多個職位,這樣它的許可權就是多個職位許可權的集合。

這樣在資料庫表設計中需要調整的是新加入職位表(職位id、職位名稱、部門id、選單許可權)和部門表(部門id、部門名稱、部門編碼、上一級部門id),並在使用者表中增加職位id、部門id。

職位表是繫結在部門下的許可權角色,選單許可權欄位直接與選單項進行關聯,不同職位可以設定不同的許可權(設定可檢視與操作的選單項)

職位表還需要儲存與部門表的關聯項:部門表id。如果為了不關聯查詢,也可以直接冗餘儲存部門編碼、和部門名稱欄位(直接儲存這個冗餘欄位,是為在需要顯示職位所屬部門時,不需要從部門表中關聯查詢,部門名稱設定後更改的機會不大,但查詢是每次都固定需要查詢),所以冗餘欄位的設定減少了查詢表次數,不過要在程式中確保兩邊表的資料更新一致。

部門表它相當於許可權分組,可以根據企業的部門結構,建立對應的結構記錄,這樣也方便企業對系統許可權關係更加容易理解。當然也可以根據需要設定虛擬部門出來管理。

為了以後擴充套件需要,需要新增部門編碼欄位,編碼從01開始一直累加到99,當然如果部門超過99個的話,要麼增加到3位數,要麼當前框架已不能支援業務的發展需要思考新的架構了。

編碼每增加一級,在01後面自動增加”0x“,編碼的長度跟部門分級深度相關。

綜合以上許可權設計思路,最終整理出的資料表關係圖為:

整個許可權控制就4張表,如果現實情況不太用得到組織部門,還可以把部門這張表去掉,並把職位表改成角色表,這樣最精簡的許可權控制資料庫表就設計完成了。

下面根據4張表來設計介面,主要有使用者管理頁面、選單管理頁面、部門管理頁面、職位管理頁面。

前端採用inspinia模板,有些外掛略有增減,對話方塊採用layer,樹選單採用ztree,表格採用bootstrap-table,具體可以看原始碼。
express中採用ejs-mate模板,是ejs擴充套件版本,支援layout。

使用者管理列表

編輯使用者

編輯使用者中主要選擇使用者所屬部門和職位,其中職位是可以多個,採用樹型結構勾選即可。

選單管理列表

選單列表採用bootstrap-table,並使用tree-grid外掛顯示選單層級關係。

編輯選單

編輯選單主要是設定上下級選單關係,頁面地址和控制元件地址,如果這個菜單隻有控制元件地址,不在左側樹選單顯示的,需要將是否顯示設為隱藏。

部門管理列表

職位管理列表

左側可以通過點選部門樹,來篩選該部門下的職位列表,方便顯示。

編輯職位

編輯職位主要設定該職位名稱及該職位所擁有的選單項,選單項是一個樹型結構,打勾即表示該職位擁護此選單項許可權。

以上是最終所實現的介面效果。大部分都屬於簡單的列表和增刪改頁面,稍微複雜點是有些頁面會涉及到樹形選單載入。

使用者登入後,就獲得了使用者所屬職位的選單項集合,在使用者每次請求路由中需增加許可權判斷,我們寫在中介軟體中。

/** 使用者鑑權*/
async authUserPermission(req, res, next) {
    if (!req.session || !req.session.user || !req.session.user.id) {
        return res.redirect('/login');
    }
    if (!req.session || !req.session.menu || req.session.menu.length == 0) {
        return res.send('抱歉,您無此許可權!請聯絡管理員');
    }
    let targetUrl = req.route.path;
    let hasPower = false;
    req.session.menu.forEach(el => {
        if (el.page_url == targetUrl || el.control_url == targetUrl) {
            hasPower = true;
        }

    });
    if (!hasPower) {
        if (req.xhr) {
            return res.json({
                state: false,
                msg: "抱歉,您無此許可權!請聯絡管理員"
            });
        }

        return res.send('抱歉,您無此許可權!請聯絡管理員');
    }
    next();
}

在每次路由請求中先判斷使用者許可權,如有許可權則往下執行正常邏輯,如無許可權直接返回。

router.get('/system/userList', auth.authUserPermission, system.showUserList);
router.get('/system/userEdit/:id', auth.authUserPermission, system.showUserEdit);

test賬號新增使用者報無許可權演示:

專案原始碼地址:https://github.com/ciey/NodeExpressAd