Tips:本篇已加入系列文章閱讀目錄,可點選檢視更多相關文章。
前言
上一篇已經建立好了前後端專案,本篇開始編碼部分。
開始
幾乎所有的系統都繞不開登入功能,那麼就從登入開始,完成使用者登入以及使用者選單許可權控制。
登入
首先使用者輸入賬號密碼點選登入,然後組合以下引數呼叫identityserver的/connect/token
端點獲取token:
{
grant_type: "password",
scope: "HelloAbp",
username: "",
password: "",
client_id: "HelloAbp_App",
client_secret: "1q2w3e*"
}
這個引數來自ABP模板的種子資料:
我使用的是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增加跨域配置:
前端部分需要修改的檔案太多,下面只貼出部分主要程式碼,需要完整原始碼的可以去GitHub拉取。
src\store\modules\user.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:
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:
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:
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欄位,與對應的選單路由繫結,就可以實現許可權控制了。
src\permission.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:
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:
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*登入系統:
正常的話就可以進入這個介面了:
目前可以看到“許可權測試頁”選單,因為現在還沒有設定許可權的介面,所以我手動去資料庫把這條許可權資料刪除,然後測試一下:
但是手動去資料庫改這個表的話會有很長一段時間的快取,在redis中,暫時沒去研究這個快取機制,正常通過介面修改應該不會這樣。。。
我手動清理了redis,執行結果如下:
最後
本篇實現了前端部分的登入和選單許可權控制,但是還有很多細節問題需要處理。比如右上角的使用者頭像,ABP的預設使用者表中是沒有頭像和使用者介紹欄位的,下篇將完善這些問題,還有刪除掉vue-element-admin多餘的選單。