1. 程式人生 > >初識ABP vNext(4):vue使用者登入&選單許可權

初識ABP vNext(4):vue使用者登入&選單許可權

Tips:本篇已加入系列文章閱讀目錄,可點選檢視更多相關文章。 [TOC] # 前言 上一篇已經建立好了前後端專案,本篇開始編碼部分。 # 開始 幾乎所有的系統都繞不開登入功能,那麼就從登入開始,完成使用者登入以及使用者選單許可權控制。 ## 登入 首先使用者輸入賬號密碼點選登入,然後組合以下引數呼叫identityserver的`/connect/token`端點獲取token: ```json { grant_type: "password", scope: "HelloAbp", username: "", password: "", client_id: "HelloAbp_App", client_secret: "1q2w3e*" } ``` 這個引數來自ABP模板的種子資料: ![](https://img2020.cnblogs.com/blog/610959/202008/610959-20200812101059692-735307811.png) ![](https://img2020.cnblogs.com/blog/610959/202008/610959-20200812101202182-1379040031.png) 我使用的是password flow,這個flow無需重定向。如果你的網站應用只有一個的話,可以這麼做,如果有多個的話建議採用其他oidc方式,把認證介面放到identityserver程式裡,客戶端重定向到identityserver去認證,這樣其實更安全,並且你無需在每個客戶端網站都做一遍登入介面和邏輯。。。 還有一點,嚴格來說不應該直接訪問`/connect/token`端點獲取token。首先應該從identityserver發現文件`/.well-known/openid-configuration`中獲取配置資訊,然後從`/.well-known/openid-configuration/jwks`端點獲取公鑰等資訊用於校驗token合法性,最後才是獲取token。ABP的Angular版本就是這麼做的,不過他是使用`angular-oauth2-oidc`這個庫完成,我暫時沒有找到其他的支援password flow的開源庫,參考:https://github.com/IdentityModel/oidc-client-js/issues/234 前端想正常訪問介面,首先需要在HttpApi.Host,IdentityServer增加跨域配置: ![](https://img2020.cnblogs.com/blog/610959/202008/610959-20200812102321207-1018751145.png) ![](https://img2020.cnblogs.com/blog/610959/202008/610959-20200812102152761-331590081.png) 前端部分需要修改的檔案太多,下面只貼出部分主要程式碼,需要完整原始碼的可以去GitHub拉取。 src\store\modules\user.js: ```js const clientSetting = { grant_type: "password", scope: "HelloAbp", username: "", password: "", client_id: "HelloAbp_App", client_secret: "1q2w3e*" }; const actions = { // user login login({ commit }, userInfo) { const { username, password } = userInfo return new Promise((resolve, reject) => { clientSetting.username = username.trim() clientSetting.password = password login(clientSetting) .then(response => { const data = response commit('SET_TOKEN', data.access_token) setToken(data.access_token).then(() => { resolve() }) }) .catch(error => { reject(error) }) }) }, // get user info getInfo({ commit }) { return new Promise((resolve, reject) => { getInfo() .then(response => { const data = response if (!data) { reject('Verification failed, please Login again.') } const { name } = data commit('SET_NAME', name) commit('SET_AVATAR', '') commit('SET_INTRODUCTION', '') resolve(data) }) .catch(error => { reject(error) }) }) }, setRoles({ commit }, roles) { commit('SET_ROLES', roles) }, // user logout logout({ commit, dispatch }) { return new Promise((resolve, reject) => { logout() .then(() => { commit('SET_TOKEN', '') commit('SET_NAME', '') commit('SET_AVATAR', '') commit('SET_INTRODUCTION', '') commit('SET_ROLES', []) removeToken().then(() => { resetRouter() // reset visited views and cached views // to fixed https://github.com/PanJiaChen/vue-element-admin/issues/2485 dispatch('tagsView/delAllViews', null, { root: true }) resolve() }) }) .catch(error => { reject(error) }) }) }, // remove token resetToken({ commit }) { return new Promise(resolve => { commit('SET_TOKEN', '') commit('SET_NAME', '') commit('SET_AVATAR', '') commit('SET_INTRODUCTION', '') commit('SET_ROLES', []) removeToken().then(() => { resolve() }) }) } } ``` src\utils\auth.js: ```js export async function setToken(token) { const result = Cookies.set(TokenKey, token); await store.dispatch("app/applicationConfiguration"); return result; } export async function removeToken() { const result = Cookies.remove(TokenKey); await store.dispatch("app/applicationConfiguration"); return result; } ``` src\api\user.js: ```js export function login(data) { return request({ baseURL: "https://localhost:44364", url: "/connect/token", method: "post", headers: { "content-type": "application/x-www-form-urlencoded" }, data: qs.stringify(data), }); } export function getInfo() { return request({ url: "/api/identity/my-profile", method: "get", }); } export function logout() { return request({ baseURL: "https://localhost:44364", url: "/api/account/logout", method: "get", }); } ``` src\utils\request.js: ```js service.interceptors.request.use( (config) => { // do something before request is sent if (store.getters.token) { config.headers["authorization"] = "Bearer " + getToken(); } return config; }, (error) => { // do something with request error console.log(error); // for debug return Promise.reject(error); } ); // response interceptor service.interceptors.response.use( (response) => { const res = response.data; return res; }, (error) => { console.log("err" + error); // for debug Message({ message: error.message, type: "error", duration: 5 * 1000, }); if (error.status === 401) { // to re-login MessageBox.confirm( "You have been logged out, you can cancel to stay on this page, or log in again", "Confirm logout", { confirmButtonText: "Re-Login", cancelButtonText: "Cancel", type: "warning", } ).then(() => { store.dispatch("user/resetToken").then(() => { location.reload(); }); }); } return Promise.reject(error); } ); ``` ## 選單許可權 vue-element-admin的選單許可權是使用使用者角色來控制的,我們不需要role。前面分析過,通過`/api/abp/application-configuration`介面的auth.grantedPolicies欄位,與對應的選單路由繫結,就可以實現許可權控制了。 ![](https://img2020.cnblogs.com/blog/610959/202008/610959-20200812105328542-1868756016.png) src\permission.js: ```js router.beforeEach(async (to, from, next) => { // start progress bar NProgress.start(); // set page title document.title = getPageTitle(to.meta.title); let abpConfig = store.getters.abpConfig; if (!abpConfig) { abpConfig = await store.dispatch("app/applicationConfiguration"); } if (abpConfig.currentUser.isAuthenticated) { if (to.path === "/login") { // if is logged in, redirect to the home page next({ path: "/" }); NProgress.done(); // hack: https://github.com/PanJiaChen/vue-element-admin/pull/2939 } else { //user name const name = store.getters.name; if (name) { next(); } else { try { // get user info await store.dispatch("user/getInfo"); store.dispatch("user/setRoles", abpConfig.currentUser.roles); const grantedPolicies = abpConfig.auth.grantedPolicies; // generate accessible routes map based on grantedPolicies const accessRoutes = await store.dispatch( "permission/generateRoutes", grantedPolicies ); // dynamically add accessible routes router.addRoutes(accessRoutes); // hack method to ensure that addRoutes is complete // set the replace: true, so the navigation will not leave a history record next({ ...to, replace: true }); } catch (error) { // remove token and go to login page to re-login await store.dispatch("user/resetToken"); Message.error(error || "Has Error"); next(`/login?redirect=${to.path}`); NProgress.done(); } } } } else { if (whiteList.indexOf(to.path) !== -1) { // in the free login whitelist, go directly next(); } else { // other pages that do not have permission to access are redirected to the login page. next(`/login?redirect=${to.path}`); NProgress.done(); } } }); ``` src\store\modules\permission.js: ```js function hasPermission(grantedPolicies, route) { if (route.meta && route.meta.policy) { const policy = route.meta.policy; return grantedPolicies[policy]; } else { return true; } } export function filterAsyncRoutes(routes, grantedPolicies) { const res = []; routes.forEach((route) => { const tmp = { ...route }; if (hasPermission(grantedPolicies, tmp)) { if (tmp.children) { tmp.children = filterAsyncRoutes(tmp.children, grantedPolicies); } res.push(tmp); } }); return res; } const state = { routes: [], addRoutes: [], }; const mutations = { SET_ROUTES: (state, routes) => { state.addRoutes = routes; state.routes = constantRoutes.concat(routes); }, }; const actions = { generateRoutes({ commit }, grantedPolicies) { return new Promise((resolve) => { let accessedRoutes = filterAsyncRoutes(asyncRoutes, grantedPolicies); commit("SET_ROUTES", accessedRoutes); resolve(accessedRoutes); }); }, }; ``` src\router\index.js: ```js export const asyncRoutes = [ { path: '/permission', component: Layout, redirect: '/permission/page', alwaysShow: true, // will always show the root menu name: 'Permission', meta: { title: 'permission', icon: 'lock', policy: 'AbpIdentity.Roles' }, children: [ { path: 'page', component: () => import('@/views/permission/page'), name: 'PagePermission', meta: { title: 'pagePermission', policy: 'AbpIdentity.Roles' } }, { path: 'directive', component: () => import('@/views/permission/directive'), name: 'DirectivePermission', meta: { title: 'directivePermission', policy: 'AbpIdentity.Roles' } }, { path: 'role', component: () => import('@/views/permission/role'), name: 'RolePermission', meta: { title: 'rolePermission', policy: 'AbpIdentity.Roles' } } ] }, 。。。。。。 // 404 page must be placed at the end !!! { path: '*', redirect: '/404', hidden: true } ] ``` 因為選單太多了,就拿其中的一個“許可權測試頁”選單舉例,將它與AbpIdentity.Roles繫結測試。 ## 執行測試 執行前後端專案,使用預設賬號admin/1q2w3E*登入系統: ![](https://img2020.cnblogs.com/blog/610959/202008/610959-20200812114347246-1031799810.png) 正常的話就可以進入這個介面了: ![](https://img2020.cnblogs.com/blog/610959/202008/610959-20200812115855889-335942807.png) 目前可以看到“許可權測試頁”選單,因為現在還沒有設定許可權的介面,所以我手動去資料庫把這條許可權資料刪除,然後測試一下: ![](https://img2020.cnblogs.com/blog/610959/202008/610959-20200812120437552-549508790.png) 但是手動去資料庫改這個表的話會有很長一段時間的快取,在redis中,暫時沒去研究這個快取機制,正常通過介面修改應該不會這樣。。。 ![](https://img2020.cnblogs.com/blog/610959/202008/610959-20200812152715218-633964945.png) 我手動清理了redis,執行結果如下: ![](https://img2020.cnblogs.com/blog/610959/202008/610959-20200812152943841-275384445.png) # 最後 本篇實現了前端部分的登入和選單許可權控制,但是還有很多細節問題需要處理。比如右上角的使用者頭像,ABP的預設使用者表中是沒有頭像和使用者介紹欄位的,下篇將完善這些問題,還有刪除掉vue-element-admin多餘的選單。