01 Bug 描述
筆者基于簡化版的 vue-element-admin 前端框架 vue-admin-template 進行二次開發,
我在專案中設定了三個用戶角色,不同的角色具有不同的權限,在此之前專案中已經實作了不同用戶角色的權限認證以及動態路由生成:vue-element-admin 動態路由無法動態渲染側邊欄-解決記錄,
加上權限認證之后,專案就出現的了一個十分魔幻的 Bug:我使用用戶A登錄系統后,在系統內選擇退出登錄,回到登錄頁面后切換用戶B登錄系統,這時直接跳轉到404頁面,如下圖所示:

可以看到 1 中我已經成功登錄了管理員用戶,并在2中顯示了我已經成功退出登錄了管理員用戶,然后我在3中切換為學生用戶登錄系統,
點擊登錄后,出現如下圖所示情況,可以看到學生用戶已經成功登錄,但是頁面卻沒有成功跳轉,而是在404頁面,更加神奇的是,這個 Bug 時有時無,并不是每次切換用戶都會出現這種情況,讓人摸不著頭腦,

02 登錄權限流程
毫無疑問,登出切換用戶后重新登錄跳轉404頁面的Bug肯定是出現在登錄邏輯和權限認證程序中,所以接下來我們梳理一下 vue-element-admin 登錄邏輯和權限認證流程,
2.1 vue-element-admin 登錄邏輯
vue-element-admin 登錄邏輯如下圖所示:
- 首先在登錄頁面點擊按鈕后觸發點擊事件,然后在事件中分發store.action
- 在 store 的對應 action 中呼叫 axios 介面獲取后臺資料,如果登錄成功則更具用戶角色創建 token,并將這些認證資訊保存到 cookies 中
- 獲取 token 之后在
@/src/permission.js獲取用戶資訊并進行權限認證生成動態路由

2.1.1 登錄頁面點擊按鈕后觸發點擊事件
在 @/src/views/login/index.vue 中查看登錄頁面點擊按鈕后觸發點擊事件,并在事件中分發 store.action 實作相關代碼如下:
<template>
<div class="login-container">
<el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form" autocomplete="on" label-position="left">
<--! 登錄頁面點擊按鈕后觸發點擊事件 -->
<el-button :loading="loading" type="primary" style="width:100%;margin-bottom:30px;" @click.native.prevent="handleLogin">Login</el-button>
</el-form>
</div>
</template>
<script>
import { validUsername } from '@/utils/validate'
export default {
name: 'Login',
watch: {
$route: {
handler: function(route) {
this.redirect = route.query && route.query.redirect
},
immediate: true
}
},
methods: {
// 在事件中分發 store.action
handleLogin() {
this.$refs.loginForm.validate(valid => {
if (valid) {
this.loading = true
// 分發 store.action
this.$store.dispatch('user/login', this.loginForm).then(() => {
this.$router.push({ path: this.redirect || '/' })
this.loading = false
}).catch(() => {
this.loading = false
})
} else {
console.log('error submit!!')
return false
}
})
}
}
}
</script>
2.1.2 在 store 的對應 action 中呼叫 axios 介面獲取后臺資料
在 @/src/store/modules/user.js 的對應 action 中呼叫 axios 介面獲取后臺資料,相關實作代碼如下:
import { login, logout, getInfo } from '@/api/user'
import { getToken, setToken, removeToken } from '@/utils/auth'
import router, { resetRouter } from '@/router'
const state = {
token: getToken(),
name: '',
avatar: '',
introduction: '',
roles: []
}
const mutations = {
SET_TOKEN: (state, token) => {
state.token = token
},
SET_INTRODUCTION: (state, introduction) => {
state.introduction = introduction
},
SET_NAME: (state, name) => {
state.name = name
},
SET_AVATAR: (state, avatar) => {
state.avatar = avatar
},
SET_ROLES: (state, roles) => {
state.roles = roles
}
}
const actions = {
// 登錄 action
login({ commit }, userInfo) {
const { username, password } = userInfo
return new Promise((resolve, reject) => {
// 在登錄 action 中呼叫 axios 介面獲取后臺資料,如果獲取成功則創建 tocken 并保存在 cookie 中
login({ username: username.trim(), password: password }).then(response => {
const { data } = response
commit('SET_TOKEN', data.token)
setToken(data.token)
resolve()
}).catch(error => {
reject(error)
})
})
},
// 獲取用戶資訊 action
getInfo({ commit, state }) {
return new Promise((resolve, reject) => {
getInfo(state.token).then(response => {
const { data } = response
if (!data) {
reject('Verification failed, please Login again.')
}
const { roles, name, avatar, introduction } = data
if (!roles || roles.length <= 0) {
reject('getInfo: roles must be a non-null array!')
}
commit('SET_ROLES', roles)
commit('SET_NAME', name)
commit('SET_AVATAR', avatar)
commit('SET_INTRODUCTION', introduction)
resolve(data)
}).catch(error => {
reject(error)
})
})
}
}
export default {
namespaced: true,
state,
mutations,
actions
}
2.1.3 登錄 axios 介面定義
在 @/src/api/user.js 查看登錄的 axios 請求介面,實作代碼如下:
import request from '@/utils/request'
// 登錄 api 請求
export function login(data) {
return request({
url: '/vue-element-admin/user/login',
method: 'post',
data
})
}
// 獲取用戶資訊 api 請求
export function getInfo(token) {
return request({
url: '/vue-element-admin/user/info',
method: 'get',
params: { token }
})
}
2.1.4 獲取 token 后獲取用戶資訊
用戶登錄成功之后,在全域鉤子 router.beforeEach 中攔截路由,判斷是否已獲得token,在獲得token 之后就在 @/src/permission.js 獲取用戶資訊,相關代碼如下:
import router from './router'
import store from './store'
import { getToken } from '@/utils/auth' // get token from cookie
router.beforeEach(async(to, from, next) => {
const hasToken = getToken()
if (hasToken) {
const hasRoles = store.getters.roles && store.getters.roles.length > 0
if (hasRoles) {
next()
} else {
try {
// 分發 store.action 獲取用戶資訊
const { roles } = await store.dispatch('user/getInfo')
const accessRoutes = await store.dispatch('permission/generateRoutes', roles)
router.addRoutes(accessRoutes)
next({ ...to, replace: true })
} catch (error) {
await store.dispatch('user/resetToken')
Message.error(error || 'Has Error')
next(`/login?redirect=${to.path}`)
}
}
}
})
2.2 vue-element-admin 權限認證流程
vue-element-admin 的權限認證流程入下圖所示:
- 在專案入口的
@/src/main.js中創建 vue 實體時,將 vue-router 掛載,但這個時候 vue-router 掛載的是全域路由表,即一些登錄或者不用權限的公用的頁面, - 當用戶登錄后驗證是否攜帶 token,在
@/src/permission.js中分發user/getInfo行為獲取用戶 role,然后將 role 作為輸入分發permission/generateRoutes行為將 role 和路由表每個頁面的需要的權限作比較,生成最終用戶可訪問的動態路由表, - 呼叫
router.addRoutes(store.getters.addRouters)添加用戶可訪問的路由, - 使用 vuex 管理路由表,根據 vuex 中可訪問的路由渲染側邊欄組件,

2.2.1 入口掛載全域路由表
在專案入口的 @/src/main.js 中創建 vue 實體時,將 vue-router 掛載,但這個時候 vue-router 掛載的是全域路由表,即一些登錄或者不用權限的公用的頁面,相關代碼如下:
import Vue from 'vue'
import App from './App'
import store from './store'
// 引入全域路由表
import router from './router'
new Vue({
el: '#app',
router, // 掛載全域路由表
store,
render: h => h(App)
})
2.2.2 權限認證
在 @/src/permission.js 中要完成如下權限認證任務:
- 當用戶登錄后驗證是否攜帶 token
- 若攜帶則分發
user/getInfo行為獲取用戶 role 等相關資訊 - 然后將 role 作為輸入分發
permission/generateRoutes行為,獲取用戶角色對應的動態路由表 - 呼叫
router.addRoutes(store.getters.addRouters)添加用戶可訪問的路由
相關實作代碼如下:
import router from './router'
import store from './store'
import { getToken } from '@/utils/auth' // get token from cookie
router.beforeEach(async(to, from, next) => {
// 驗證是否攜帶 token
const hasToken = getToken()
if (hasToken) {
// 已經登錄 從 state 中獲取用戶角色
const hasRoles = store.getters.roles && store.getters.roles.length > 0
if (hasRoles) {
next()
} else {
try {
// 首次登錄,分發 user/getInfo 行為獲取用戶 role 等相關資訊
const { roles } = await store.dispatch('user/getInfo')
// 將 role 作為輸入分發 `permission/generateRoutes` 行為,獲取用戶角色對應的動態路由表
const accessRoutes = await store.dispatch('permission/generateRoutes', roles)
// 添加用戶可訪問的路由
router.addRoutes(accessRoutes)
next({ ...to, replace: true })
} catch (error) {
await store.dispatch('user/resetToken')
Message.error(error || 'Has Error')
next(`/login?redirect=${to.path}`)
}
}
}
})
2.2.3 權限認證與動態路由實作
實作權限認證與動態路由大致程序分為如下四個步驟:
-
修改
src/store/modules/user.js增加用戶資訊中角色的權限串列 roles -
修改
src/router/index.js根據用戶角色劃分路由 -
增加
src/store/modules/permission.js通過獲取當前用戶的權限去比對路由表,生成當前用戶具有訪問權限的動態路由表 -
修改
src/permission.js通過router.addRoutes將用戶可訪問路由表動態掛載到 router 上
詳細程序筆者已在 vue-element-admin 動態路由無法動態渲染側邊欄-解決記錄 中的第二節 (動態路由修改程序) 介紹過了,有興趣可以進一步了解,
03 發現 Bug
啰嗦啰嗦一大堆回顧了 vue-element-admin 登錄權限的實作邏輯與代碼,似乎并沒有找到這個時有時無的魔幻 Bug 的出處,但是也找到了一些蛛絲馬跡:
- 出現在登錄業務,且時有時無的 Bug 癥狀,說明一定與動態路由有關,只有動態變化才會出現一會兒正常一會兒魔怔
- 能夠成功登錄,且回傳了已經登錄的用戶資訊,說明
@/src/store/modules/user.js中的用戶登錄行為以及用戶資訊獲取行為都正確執行了;同時也說明@/src/permission.js中的權限認證流程都成功執行了
那么 Bug 的原因就直指登錄成功之后的頁面跳轉程序,登錄成功后的跳轉僅在 登錄頁面點擊按鈕后觸發點擊事件 中有定義,即 @/src/views/login/index.vue 中如下所示:
<template>
<div class="login-container">
<el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form" autocomplete="on" label-position="left">
<--! 登錄頁面點擊按鈕后觸發點擊事件 -->
<el-button :loading="loading" type="primary" style="width:100%;margin-bottom:30px;" @click.native.prevent="handleLogin">Login</el-button>
</el-form>
</div>
</template>
<script>
import { validUsername } from '@/utils/validate'
export default {
name: 'Login',
watch: {
$route: {
handler: function(route) {
this.redirect = route.query && route.query.redirect
},
immediate: true
}
},
methods: {
// 在事件中分發 store.action
handleLogin() {
this.$refs.loginForm.validate(valid => {
if (valid) {
this.loading = true
// 分發 store.action
this.$store.dispatch('user/login', this.loginForm).then(() => {
// 登錄成功后,跳轉到 this.redirect 或者 / 路徑下
this.$router.push({ path: this.redirect || '/' })
this.loading = false
}).catch(() => {
this.loading = false
})
} else {
console.log('error submit!!')
return false
}
})
}
}
}
</script>
Bug 的關鍵就在于這行代碼 this.$router.push({ path: this.redirect || '/' }) ,登錄成功之后可以跳轉到 this.redirect
再看與其相關的 watch 屬性,一直在監聽路由傳的值和重定向路徑,是不是已經發現了 Bug 所在?
watch: {
$route: {
handler: function(route) {
this.redirect = route.query && route.query.redirect
},
immediate: true
}
}
04 解決 Bug
Bug 的成因
我通過如下示例來解釋 Bug 的成因:
我們假設專案中全域路由即所有用戶共用的路由是 ‘/’,'/login','/404'
用戶 A 具有權限的路由是 ‘/userinfoA’,用戶 B 具有權限的路由是 ‘/userinfoB
如果用戶 A 登錄之后并在路由 ‘/userinfoA’ 所指向的頁面中登出跳轉到登錄頁面,這時$route.query.redirect 保存的路由就是 ‘/userinfoA’
當在登出后的登錄頁面登錄用戶 B 時,此時的登錄頁面點擊按鈕后觸發點擊事件完成用戶登錄之后,在選擇跳轉頁面時選擇了路由 ‘/userinfoA’ 指向的頁面,而用戶 B 不具有該頁面權限所以直接跳轉到了 404 頁面,
解決 Bug
一種簡單的解決方式就是不使用登出前的路由,不管是首次登錄還是切換用戶登錄,所有登錄業務完成之后都跳轉到共用路徑 ‘/’ 所指向的頁面,洗掉 @/src/views/login/index.vue 中重定向相關即可,如下所示:
<template>
<div class="login-container">
<el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form" autocomplete="on" label-position="left">
<--! 登錄頁面點擊按鈕后觸發點擊事件 -->
<el-button :loading="loading" type="primary" style="width:100%;margin-bottom:30px;" @click.native.prevent="handleLogin">Login</el-button>
</el-form>
</div>
</template>
<script>
import { validUsername } from '@/utils/validate'
export default {
name: 'Login',
// watch: {
// $route: {
// handler: function(route) {
// this.redirect = route.query && route.query.redirect
// },
// immediate: true
// }
// },
methods: {
// 在事件中分發 store.action
handleLogin() {
this.$refs.loginForm.validate(valid => {
if (valid) {
this.loading = true
// 分發 store.action
this.$store.dispatch('user/login', this.loginForm).then(() => {
// 登錄成功后,跳轉到 this.redirect 或者 / 路徑下
// this.$router.push({ path: this.redirect || '/' })
this.$router.push({ path: '/' })
this.loading = false
}).catch(() => {
this.loading = false
})
} else {
console.log('error submit!!')
return false
}
})
}
}
}
</script>
當然,你也可以通過在@/src/router/index.js 實作不保存redirect 的路由構建方法,可能會稍微復雜一些,
參考資料
vue-element-admin 官方檔案:權限驗證
手摸手,帶你用vue擼后臺 系列二(登錄權限篇)
vue Element Admin 登錄、驗證流程
轉載請註明出處,本文鏈接:https://www.uj5u.com/qianduan/377098.html
標籤:其他
