管理 JWT Token 是前端不可避免的課題,若只透過 Vuex 或 Local Storage 都有其缺憾,唯有同時結合 Vuex 與 Local Storage 才是最完美方案。
Version
macOS Catalina 10.15.4
WebStorm 2020.1
Vue 2.6.11
Axios 0.19.2
Challenges
雖然可用 local storage 儲存 token,當需要 token 時就存取 local storage,但這種方式並不優雅
只使用 Vuex 存 token 雖然方便,但只要在 development mode 修改重新執行,token 就會不見
沒有 token 或 token expired 只能存取
/login
如何避免每個 API 都要傳入 token ?
當 login 時,自動設定所有 API call 都有共用 token;當 logout 時,移除所有共用 token
Vuex
auth.js
let setToken = (state, { token }) => state.token = token
let isAuth = state => !!state.token
let login = ({ commit }, { token }) => {
localStorage.setItem('token', token)
commit('setToken', { token })
}
let logout = ({ commit }) => {
localStorage.removeItem('token')
commit('setToken', { token: '' })
}
export default {
namespaced: true,
state: {
token: localStorage.getItem('token') || '',
},
mutations: {
setToken,
},
getters: {
isAuth
},
actions: {
login,
logout
}
}
17 行
state: {
token: localStorage.getItem('token') || '',
},
token
state 骨子裡一樣使用 local storage,若 local storage 不存在則為 empty string。
Vuex 搭配 local storage 可避免 development mode 修改時重新執行 token 不見,此時會自動讀取 local storage 重建
token
state
第 1 行
let setToken = (state, { token }) => state.token = token
實現 setToken()
mutation。
第 5 行
let login = ({ commit }, { token }) => {
localStorage.setItem('token', token)
commit('setToken', { token })
}
實現 login
action,當 login 成功取得 token 時呼叫,有兩個目的:
- 寫入 local storage,屬於 side effect
- 寫入
token
state
第 10 行
let logout = ({ commit }) => {
localStorage.removeItem('token')
commit('setToken', { token: '' })
}
實現 logout
action,當 logout 時呼叫,有兩個目的:
- 清空 local storage,屬於 side effect
- 將 empty string 寫入
token
state
第 3 行
let isAuth = state => !!state.token
實現 isAuth
token,判斷 token 是否存在,主要用於沒有 token 只能存取 /login
Login
let login = function() {
pipe(
loginAccount,
andThen(x => x.data.access_token),
andThen(x => {
this.$store.dispatch('auth/login', { token: x })
this.$store.dispatch('user/login', { username: this.username })
}),
andThen(_ => this.$router.push('/face/history')),
otherwise(x => x && x.response && console.log(x.response.data))
)({
username: this.username,
password: this.password
})
}
第 3 行
loginAccount,
為 API function,負責傳入 username 與 password 取得 token。
第 6 行
this.$store.dispatch('auth/login', { token: x })
將 token 傳入 auth/login
action 登入。
Logout
let logout = function() {
swal({
title: '你確定要登出嗎 ?',
type: 'warning',
showCancelButton: true,
confirmButtonClass: 'btn btn-success btn-fill',
cancelButtonClass: 'btn btn-danger btn-fill',
confirmButtonText: '確定',
buttonsStyling: false
}).then((function() {
this.$store.dispatch('auth/logout')
this.$router.push('/')
}).bind(this)
).catch(x => console.log(x))
}
11 行
this.$store.dispatch('auth/logout')
呼叫 auth/logout
action 登出。
Auto-authentication
App.vue
let created = () => {
axios.interceptors.request.use(x => {
let token = store.state.auth.token
token && (x.headers.Authorization = 'Bearer ' + token)
return x
}, e => Promise.reject(e))
axios.interceptors.response.use(undefined, function (err) {
return new Promise(function () {
if (err.status === 401 && err.config && !err.config.__isRetryRequest) {
this.$store.dispatch('auth/logout')
this.$router.push('/')
}
throw err
})
})
}
第 2 行
axios.interceptors.request.use(x => {
let token = store.state.auth.token
token && (x.headers.Authorization = 'Bearer ' + token)
return x
}, e => Promise.reject(e))
當每次使用 Axios 時,會自動從 Vuex 取得 token,不必自行準備 token 給 API function。
第 8 行
axios.interceptors.response.use(undefined, function (err) {
return new Promise(function () {
if (err.status === 401 && err.config && !err.config.__isRetryRequest) {
this.$store.dispatch('auth/logout')
this.$router.push('/')
}
throw err
})
})
當 token expired 時,會自動呼叫 auth/logout
action 登出。
Authenticated Routes
routes.js
let auth = (to, from, next) => {
if (store.getters['auth/isAuth']) {
next()
return
}
next('/login')
}
let routes = [
{
path: '/',
name: 'Root',
redirect: '/login'
},
{
path: '/login',
name: 'Login',
component: Login
},
{
path: '/face',
beforeEnter: auth,
component: () => import('src/views/face/RecognizeFace.vue')
}
]
第 1 行
let auth = (to, from, next) => {
if (store.getters['auth/isAuth']) {
next()
return
}
next('/login')
}
由 auth/isAuth
getter 判斷是否已經通過驗證,若通過驗證則維持原來 route,否則自動導到 /login
。
21 行
{
path: '/face',
beforeEnter: auth,
component: () => import('src/views/face/RecognizeFace.vue')
}
再要驗證的 route 加上 beforeEnter
property,並將 auth()
指定給它,如此只有通過驗證才能進入該 route。
Conclusion
- Vuex 在實務上或許沒那麼常用,但以 Vuex 管理 JWT token 則非常方便優雅
Reference
Thibaud, Authentication Best Practices for Vue
Chris Nwamba, Handling Authentication in Vue Using Vuex
蘇靚軒 Jenson, Vue 專案中的 API 管理及封裝 - JWT 身份驗證篇