摘要:按鈕方面按鈕通過自定義指令綁定其特定的操作接口信息如產(chǎn)品上傳按鈕,需要擁有產(chǎn)品上傳的信息,才可以繼續(xù)執(zhí)行按鈕的業(yè)務(wù)邏輯。
開篇啰嗦幾句
在傳統(tǒng)單體項目中,通常會有一些框架用來管理熟知的權(quán)限。如耳濡目染的 Shiro 或者 Spring Security 。然而,到了現(xiàn)在這個時代,新開始的項目會更多的才用后端微服務(wù) + 前端 mvvm 的架構(gòu)開始書寫項目。權(quán)限控制方面將變得有些許晦澀。當(dāng)然,得益于 Java 后端的 Spring 項目的可插拔式接口以及前端大部分 SPA 的架構(gòu),權(quán)限控制也并沒有變得那么的難以實(shí)現(xiàn)。相反,在我看來只要找到一個合適的插拔插件的入口,權(quán)限也可以做的很簡單,跟業(yè)務(wù)只要少許的耦合即可實(shí)現(xiàn)我們熟悉的權(quán)限模塊。
示例代碼 iview-admin-permission
實(shí)現(xiàn)思路 路由方面后端提供當(dāng)前登錄用戶可訪問的所有可訪問資源接口信息,前端項目根據(jù)當(dāng)前用戶所擁有的接口信息,判斷是否擁有菜單權(quán)限,如果擁有主要訪問權(quán)限則加入路由,讓用戶可以訪問其當(dāng)前的頁面。
由于 iview-admin 中,路由信息與菜單是同一份資料,所以在加入路由的時候,即可實(shí)現(xiàn)菜單的隱藏與現(xiàn)實(shí)。
按鈕方面按鈕通過 vue 自定義指令綁定其特定的操作接口信息(如:產(chǎn)品上傳按鈕,需要擁有產(chǎn)品上傳的信息,才可以繼續(xù)執(zhí)行按鈕的業(yè)務(wù)邏輯)。在按鈕或者 a 標(biāo)簽被渲染的時候,判斷是否擁有權(quán)限,如若沒有,可以隱藏,或者劫持原來綁定的點(diǎn)擊事件,使其變成無權(quán)限的彈窗提示。
該指令支持 render 生成的按鈕或者 a 標(biāo)簽。
數(shù)據(jù)表格方面數(shù)據(jù)的隱藏與否應(yīng)該不會放在前端做吧...不然就是沙雕網(wǎng)友了。
這方面可以提供思路,在微服務(wù)架構(gòu)中,如若前端項目擁有 NodeJS 做數(shù)據(jù)轉(zhuǎn)換層,那是極好的。如果沒有,則需要在微服務(wù)路由層做一些細(xì)微的改變。即用戶無權(quán)限的數(shù)據(jù)列(屬性)給他模糊掉,變成 * 或者 — 輸出。這方面與前端無關(guān),那么并不會在這里出現(xiàn)。
優(yōu)缺點(diǎn)優(yōu)點(diǎn):
后端不再關(guān)注頁面上的事情,基于權(quán)限代碼讓前端自己隨心所欲編排
靈活性較高,可以控制頁面上所有的元素,可以隱藏彈窗等等喜歡的功能
缺點(diǎn):
編排路由的時候會顯得繁瑣,權(quán)限代碼比較零散,開發(fā)完業(yè)務(wù)頁面可能會忘記加上
代碼實(shí)現(xiàn)公司實(shí)現(xiàn)并不能成為開源,所以我重新下載一份 iview-admin ,然后加入關(guān)鍵代碼,來實(shí)現(xiàn)上面所需要的功能。
常規(guī)操作:
npm install npm run dev
正常打開窗口即可進(jìn)行修改。
接下來就小步前進(jìn)加入權(quán)限的功能。
新增權(quán)限模塊的store權(quán)限提供了一個用戶所擁有的所有權(quán)限列表以及一個 getters 后面會用到。
export default { state: { // { operatorCode: "ProductEndpoint#upload", name: "產(chǎn)品上傳" } operatorList: [] }, getters: { hasPermission: (state) => (queryOpcode) => { if (!state.operatorList || !state.operatorList.length) { return false } return state.operatorList.map(operatInfo => operatInfo.operatorCode).indexOf(queryOpcode) > -1 } }, mutations: { setPermissionList (state, opList) { state.operatorList = opList } } }
在這里我定義簡單的后端返回值:一個 operatorCode 代表某個操作的代號,一個 name 用于分配權(quán)限的時候給用戶查看。
operatorCode 有很多中生成方式,我這里采用 SpringMVC 的 Controller 名 + # + 方法名 的形式, Controller 名用于分割不同資源的資源空間。使用 Spring 的生命周期函數(shù),在項目啟動的時候掃描,寫入數(shù)據(jù)庫,相當(dāng)于所有的資源。后面的權(quán)限模型,以前該怎樣就還是怎樣,比如用戶 —> 角色 —> 資源的模型。然后根據(jù)登陸的用戶 ID 查詢,返回給前端。至于中文名字,我們項目搭配 Swagger 使用,如果沒有使用 Swagger 則需要使用自定義開發(fā)的注解指定。
注冊到 store 中:
import Vue from "vue" import Vuex from "vuex" import user from "./module/user" import app from "./module/app" import permission from "./module/permission" Vue.use(Vuex) export default new Vuex.Store({ state: { // }, mutations: { // }, actions: { // }, modules: { user, app, permission } })用戶登陸時初始化擁有的資源操作
靜態(tài)操作數(shù)據(jù):
// src/mock/data/permission-data.js export const getPermissionCodeList = (userName) => { if (String(userName) === "1") { return [{ operatorCode: "ProductManageEndpoint#createProduct", name: "產(chǎn)品上傳" }, { operatorCode: "ProductManageEndpoint#putawayProduct", name: "產(chǎn)品上架" }, { operatorCode: "ProductManageEndpoint#delistProduct", name: "產(chǎn)品下架" }] } else { return [{ operatorCode: "ProductManageEndpoint#createProduct", name: "產(chǎn)品上傳" }, { operatorCode: "ProductManageEndpoint#delistProduct", name: "產(chǎn)品下架" }, { operatorCode: "ProductManageEndpoint#deleteByUuid", name: "產(chǎn)品刪除" }] } }
登陸操作:
// src/view/login/login.vue ... handleSubmit ({ userName, password }) { this.handleLogin({ userName, password }).then(res => { this.getUserInfo().then(res => { // 用戶登陸成功時候加入操作Code this.$store.commit("setPermissionList", getPermissionCodeList(userName)) this.$router.push({ name: this.$config.homeName }) }) }) }
加入用戶列表的信息,不然不能登錄,因為作者默認(rèn)只放了兩個,當(dāng)然生產(chǎn)項目肯定不管這里啦~~
// src/mock/login.js const USER_MAP = { super_admin: { name: "super_admin", user_id: "1", access: ["super_admin", "admin"], token: "super_admin", avator: "https://file.iviewui.com/dist/a0e88e83800f138b94d2414621bd9704.png" }, admin: { name: "admin", user_id: "2", access: ["admin"], token: "admin", avator: "https://avatars0.githubusercontent.com/u/20942571?s=460&v=4" }, zhangsan: { name: "zhangsan", user_id: "3", access: ["super_admin", "admin"], token: "zhangsan", avator: "https://avatars0.githubusercontent.com/u/20942571?s=460&v=4" }, lisi: { name: "lisi", user_id: "4", access: ["super_admin", "admin"], token: "lisi", avator: "https://avatars0.githubusercontent.com/u/20942571?s=460&v=4" } } ...
加入權(quán)限數(shù)據(jù):
// src/mock/data/permission-data.js export const getPermissionCodeList = (userName) => { if (userName === "zhangsan") { return [{ operatorCode: "ProductManageEndpoint#list", name: "產(chǎn)品列表" }, { operatorCode: "ProductManageEndpoint#createProduct", name: "產(chǎn)品上傳" }, { operatorCode: "ProductManageEndpoint#putawayProduct", name: "產(chǎn)品上架" }, { operatorCode: "ProductManageEndpoint#delistProduct", name: "產(chǎn)品下架" }] } else { return [{ operatorCode: "ProductManageEndpoint#createProduct", name: "產(chǎn)品上傳" }, { operatorCode: "ProductManageEndpoint#delistProduct", name: "產(chǎn)品下架" }, { operatorCode: "ProductManageEndpoint#deleteByUuid", name: "產(chǎn)品刪除" }] } }開始加載菜單
剛開始我們先設(shè)立一個口進(jìn)入,由于項目的 menuList 掛載在 store 的 app.js 里面,所以我們需要在這里動刀。
這里修改造成兩個事情:1. 清掉項目原來的 router 的菜單,但是還可以訪問(因為內(nèi)存加載了);2. 我加入了需要權(quán)限管理的菜單,但是還不能訪問,因為路由還沒有。
// src/store/module/app.js ... getters: { menuList: (state, getters, rootState) => { const allMenus = [] const menuWithPermission = getMenuWithPermissionByRouter(routersWithPermission, rootState.permission.operatorList) allMenus.push(...menuWithPermission) return allMenus } } ...
那么這里用到一個函數(shù) getMenuWithPermissionByRouter,我決定放在 util.js 即可
// src/libs/util.js ... export const getMenuWithPermissionByRouter = (routerWithPermissionList, userOperatorList) => { return [] }
OK,運(yùn)行項目。很好,沒有出現(xiàn)錯誤,繼續(xù)前行。
我的需求是這樣的,當(dāng)一個路由,在 meta 里面的 requireCode 用戶擁有第一項操作權(quán)限的時候,是可以進(jìn)入的,也就是菜單需要加載出來。
export default [ { path: "/product", name: "product", component: Main, meta: { hideInBread: true }, children: [ { path: "list", name: "list", meta: { title: "product-list", requireCode: ["ProductManageEndpoint#list", "ProductManageEndpoint#createProduct", "ProductManageEndpoint#putawayProduct", "ProductManageEndpoint#delistProduct", "ProductManageEndpoint#deleteByUuid"] }, component: () => import("@/view/product/product-list.vue") }, { path: "add", name: "add", meta: { title: "product-add", requireCode: ["ProductManageEndpoint#createProduct"] }, component: () => import("@/view/product/product-form.vue") } ] } ]
當(dāng)這個頁面需要同時滿足兩個要求的資源操作的時候,第一項使用數(shù)組裝載。即:
requireCode: [["ProductManageEndpoint#list", "ProductManageEndpoint#createProduct"], "ProductManageEndpoint#putawayProduct", "ProductManageEndpoint#delistProduct", "ProductManageEndpoint#deleteByUuid"]
那么這個頁面需要同時滿足前面兩個的時候,才能加載出來。
那么開始編寫 getMenuWithPermissionByRouter 函數(shù)。呃,怎么說呢,先把 Aresn 的代碼拷過來改改滿足我上面的需求即可:
/** * 根據(jù)需要控制菜單的路由,獲取菜單列表 * @param routerWithPermissionList 需要權(quán)限管理的路由 * @param userOperatorList 用戶擁有的所有操作 * @returns {Array} */ export const getMenuWithPermissionByRouter = (routerWithPermissionList, userOperatorList) => { // debugger let res = [] const allOpCodeArr = userOperatorList.map(opCodeObj => opCodeObj.operatorCode) forEach(routerWithPermissionList, item => { if (!item.meta || (item.meta && !item.meta.hideInMenu)) { let obj = { icon: (item.meta && item.meta.icon) || "", name: item.name, meta: item.meta } if ((hasChild(item) || (item.meta && item.meta.showAlways))) { obj.children = getMenuWithPermissionByRouter(item.children, userOperatorList) } if (hasThisMenuPermission(item, allOpCodeArr) || hasChild(item)) res.push(obj) } }) return res } const hasThisMenuPermission = (routerInfo, allOpCodeArr) => { if (routerInfo.meta && routerInfo.meta.requireCode && routerInfo.meta.requireCode.length) { const requireCode = routerInfo.meta.requireCode[0] if (Array.isArray(requireCode)) { let hasPermission = true for (let i = 0; i < requireCode.length; i++) { hasPermission = hasPermission && allOpCodeArr.indexOf(requireCode[i]) > -1 } return hasPermission } else { return allOpCodeArr.indexOf(requireCode) > -1 } } else return false }
OK,如圖所示,已經(jīng)把菜單加載出來了。但是現(xiàn)在出現(xiàn)一個問題,就是刷新頁面的時候,什么都沒了。因為刷新頁面的時候,內(nèi)存中的 store 被置空了。
頁面加載的時候自動加載操作// src/view/single-page/home/home.vue ... mounted () { // 需要在這里加載權(quán)限信息 const userName = this.$store.state.user.userName this.$store.commit("setPermissionList", getPermissionCodeList(userName)) } ...
可喜可賀,刷新的時候,菜單已經(jīng)出來了。但是有個報錯,報 sideMenu 的 undefined 異常。是由于剛開始刷新頁面的時候,菜單還沒有出來,而代碼寫死了讀第0個元素,菜單是空數(shù)組,所以理所當(dāng)然導(dǎo)致了異常。
// src/components/main/components/side-menu/side-menu.vue 第 20 行,去掉最后屬性的或者即可
OK,我感覺最難的菜單搞定了。接下來要搞定路由。
動態(tài)新增有權(quán)限的路由有了前面的鋪墊,我感覺路由要好做很多了。
動態(tài)路由有一個調(diào)用的函數(shù)是:
router.addRoutes(routes: Array)
那么我們需要做的只是,獲取有權(quán)限的路由,然后加入到 Vue 中。
但是這段代碼有點(diǎn)問題就是每次進(jìn)入都會調(diào)用一次= =
// src/view/single-page/home/home.vue mounted () { // 需要在這里加載權(quán)限信息 const userName = this.$store.state.user.userName this.$store.commit("setPermissionList", getPermissionCodeList(userName)) const routers = getRouterWithPermission(routersWithPermission, this.$store.state.permission.operatorList) const originRouteNames = this.$router.options.routes.map(r => r.name) // 需要解決重復(fù)加入問題 if (routers && routers.length && originRouteNames.indexOf(routers[0].name) < 0) { this.$router.addRoutes(routers) } }
好了,菜單有了,路由有了,點(diǎn)擊測試能否進(jìn)入。
然后被作者的路由攔截到了-,-
// src/router/index.js const turnTo = (to, access, next) => { next() // 直接過。 }測試路由和菜單
分別登陸 zhangsan 和 lisi 從上面的測試權(quán)限數(shù)據(jù)可以看到,lisi 是進(jìn)入不了產(chǎn)品列表的。
登陸lisi強(qiáng)行進(jìn)入:
OK,正確了,如果沒有權(quán)限,肯定就沒有頁面嘛,直接404.
登陸zhangsan 按鈕的控制頁面的跳轉(zhuǎn)搞定了,接下來就要考慮按鈕的問題了。那么上面設(shè)置的 Vuex 的 hasPermission 在這里派上用場。關(guān)于按鈕的綁定,思路來源于 Vue 的自定義指令,只需要在主頁面注冊自定義指令,每次頁面加載的時候,加載到指定指令的組件,開始讀取這個組件的 operaCode 是否在當(dāng)前登陸的用戶中,如果存在,則繼續(xù)執(zhí)行,如果不存在,則拿到了節(jié)點(diǎn)的 Vnode 開始設(shè)置我們想要的東西,比如鎖定按鈕啊,劫持點(diǎn)擊事件彈出提示,或者隱藏都可以。
// src/main.js /** * 注冊權(quán)限控制指令 */ Vue.directive("opcode", { bind: function (el, opcode, vnode, oldVNode) { const requireOpCode = opcode.value // 如果用戶沒有這個操作Code的權(quán)限,那么劫持click事件,賦予彈出無權(quán)限彈窗的事件 console.log(requireOpCode); if (vnode.componentInstance === undefined || vnode.componentInstance === null) { if (!vnode.context.$store.getters.hasPermission(requireOpCode)) { vnode.data.on.click.fns = function () { Modal.warning({ title: "無權(quán)限", content: "很抱歉,您沒有這項操作的權(quán)限" }) } } } else { if (!vnode.componentInstance.$store.getters.hasPermission(requireOpCode)) { vnode.componentInstance.$off("click") vnode.componentInstance.$on("click", function () { Modal.warning({ title: "無權(quán)限", content: "很抱歉,您沒有這項操作的權(quán)限" }) }) } } } })
常規(guī)按鈕使用:
Render 函數(shù)中使用:
// src/view/product/product-list.vue prodColumns: [ { title: "商品名稱", key: "pname", align: "center", width: 300 }, { title: "操作", key: "operator", align: "center", render: (h, params) => { return h("a", { attrs: { href: "javascript:;" }, on: { click: () => { this.handleEditProduct(params.row) } }, // 在這里注冊自定義指令 directives: [ { name: "opcode", value: "ProductManageEndpoint#updateByUuid" } ] }, "編輯") } } ],
這種方式即使分頁,改變表格數(shù)據(jù),也可以被主頁面上的判斷監(jiān)聽到。
PS:在這里就可以看到了按鈕的編排的問題,即我們新增產(chǎn)品的頁面接口,是在填寫完 Form 表單的時候才開始請求的,但是我們需要在兩個地方做設(shè)置,一個是跳轉(zhuǎn)頁面(路由+菜單),一個是新增產(chǎn)品的按鈕,即需要把這個權(quán)限攔截提前到進(jìn)入頁面之前。這方面現(xiàn)在確實(shí)想不到更好的解決辦法了。
演示回顧一下權(quán)限設(shè)置數(shù)據(jù):
export const getPermissionCodeList = (userName) => { if (userName === "zhangsan") { return [{ operatorCode: "ProductManageEndpoint#list", name: "產(chǎn)品列表" }, { operatorCode: "ProductManageEndpoint#createProduct", name: "產(chǎn)品上傳" }, { operatorCode: "ProductManageEndpoint#putawayProduct", name: "產(chǎn)品上架" }, { operatorCode: "ProductManageEndpoint#delistProduct", name: "產(chǎn)品下架" }] } else { return [{ operatorCode: "ProductManageEndpoint#createProduct", name: "產(chǎn)品上傳" }, { operatorCode: "ProductManageEndpoint#delistProduct", name: "產(chǎn)品下架" }, { operatorCode: "ProductManageEndpoint#deleteByUuid", name: "產(chǎn)品刪除" }] } }
我們知道,用戶名為 zhangsan 是沒有刪除產(chǎn)品權(quán)限以及編輯產(chǎn)品的,但是前面的都有。而 lisi 用戶,沒有產(chǎn)品列表功能,所以列表都不可以進(jìn)去。
zhangsan 用戶界面:
lisi 用戶界面:
權(quán)限編排頁面建議我們知道,經(jīng)典權(quán)限模型是用戶、角色、資源的編排。那么我們可以設(shè)置一系列的角色,當(dāng)然記得給自己預(yù)留一個流氓角色可以讀取所有資源和角色的,利于管理。每個角色可以綁定不同的資源,也可以綁定不同的用戶。這樣用戶一旦登陸,即可讀取所有角色中的所有資源操作(記得去重)。
我們公司的綁定界面,通過讀取用戶所擁有的所有資源,再把 router 中綁定的讀取出來(當(dāng)然過濾掉當(dāng)前綁定用戶無權(quán)限的),然后通過加載勾選,最后發(fā)送請求給服務(wù)器做修改。這樣設(shè)置可以無限極下限,只要賦予用戶授權(quán)的權(quán)限,用戶即可一直創(chuàng)建然后綁定他想綁定的。
權(quán)限模塊建議當(dāng)前所做的都是前端需要做的工作,后端也需要配合完成接口的調(diào)用,即使前端做了設(shè)置,但是有些用戶可以繞過前端,這時候所有的權(quán)限操作就需要后端來攔截了。
作者:WeidanLi
我的博客
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/102387.html
摘要:前言本文主要使用來實(shí)現(xiàn)前后端分離的認(rèn)證登陸和權(quán)限管理,適合和我一樣剛開始接觸前后端完全分離項目的同學(xué),但是你必須自己搭建過前端項目和后端項目,本文主要是介紹他們之間的互通,如果不知道這么搭建前端項目的同學(xué)可以先找別的看一下。 前言 本文主要使用spring boot + shiro + vue來實(shí)現(xiàn)前后端分離的認(rèn)證登陸和權(quán)限管理,適合和我一樣剛開始接觸前后端完全分離項目的同學(xué),但是你必...
摘要:我所在的美團(tuán)酒店事業(yè)部去年月份成立,新的業(yè)務(wù)新的開發(fā)團(tuán)隊,這一切使得我們的前后端分離推進(jìn)的很徹底。日志監(jiān)控平臺日志監(jiān)控平臺是美團(tuán)內(nèi)部的一個日志收集系統(tǒng),目前美團(tuán)統(tǒng)一使用收集日志,具有接收格式日志的能力,而日志監(jiān)控平臺也是以格式日志來收集。 轉(zhuǎn)自:美團(tuán)技術(shù)團(tuán)隊 作者:美團(tuán)技術(shù)團(tuán)隊 分享理由:很好的分享,可見,基于Node的前后端分離的架構(gòu)是越顯流行和重要,前端攻城獅們,No...
摘要:兩者取長補(bǔ)短,所以深度學(xué)習(xí)框架在年,迎來了前后端開發(fā)的黃金時代。陳天奇在今年的中,總結(jié)了計算圖優(yōu)化的三個點(diǎn)依賴性剪枝分為前向傳播剪枝,例已知,,求反向傳播剪枝例,,求,根據(jù)用戶的求解需求,可以剪掉沒有求解的圖分支。 虛擬框架殺入從發(fā)現(xiàn)問題到解決問題半年前的這時候,暑假,我在SIAT MMLAB實(shí)習(xí)??粗乱粫号躎orch,一會兒跑MXNet,一會兒跑Theano。SIAT的服務(wù)器一般是不...
摘要:同時將用戶關(guān)聯(lián)到用戶組,從而可以在不斷變動權(quán)限的情況下,配置一次對應(yīng)關(guān)系,將用戶權(quán)限限制到單個上。這樣在返回數(shù)據(jù)的時候,所有記錄均為對其具有操作權(quán)限的對象。當(dāng)然是天生的組件化設(shè)計理念。 前言 在開講之前,先列舉幾個場景:場景一Hi,今天那個銷售總監(jiān)說要設(shè)立幾個銷售經(jīng)理的職位,然后每個經(jīng)理管理自己小組的銷售員,我們把用戶的銷售數(shù)據(jù)按組分開來吧。場景二Mario,今天那個市場部的說要分立幾...
閱讀 2894·2021-11-24 09:39
閱讀 3151·2021-11-19 10:00
閱讀 1552·2021-10-27 14:17
閱讀 1821·2021-10-14 09:43
閱讀 977·2021-09-03 10:30
閱讀 3421·2019-08-30 15:54
閱讀 2748·2019-08-30 13:05
閱讀 2021·2019-08-30 11:02