這篇文章寫一下前後端分離下的登入解決方案,目前大多數都採用請求頭攜帶 Token 的形式。
開寫之前先捋一下整理思路:
- 首次登入時,後端伺服器判斷使用者賬號密碼正確之後,根據使用者id、使用者名稱、定義好的祕鑰、過期時間生成 token ,返回給前端;
- 前端拿到後端返回的 token ,儲存在 localStroage 和 Vuex 裡;
- 前端每次路由跳轉,判斷 localStroage 有無 token ,沒有則跳轉到登入頁,有則請求獲取使用者資訊,改變登入狀態;
- 每次請求介面,在 Axios 請求頭裡攜帶 token;
- 後端介面判斷請求頭有無 token,沒有或者 token 過期,返回401;
- 前端得到 401 狀態碼,重定向到登入頁面。
我這裡前端使用 Vue ,地址:vue-token-login
後端使用阿里的 egg,地址:egg-token-login
首先,我們先輕微封裝一下 Axios:
我把 Token 存在localStroage,檢查有無 Token ,每次請求在 Axios 請求頭上進行攜帶
if (window.localStorage.getItem('token')) {
Axios.defaults.headers.common['Authorization'] = `Bearer ` + window.localStorage.getItem('token')
}複製程式碼
使用 respone 攔截器,對 2xx 狀態碼以外的結果進行攔截。
如果狀態碼是401,則有可能是 Token 過期,跳轉到登入頁。
instance.interceptors.response.use( response =>
{
return response
}, error =>
{
if (error.response) {
switch (error.response.status) {
case 401: router.replace({
path: 'login', query: {
redirect: router.currentRoute.fullPath
} // 將跳轉的路由path作為引數,登入成功後跳轉到該路由
})
}
} return Promise.reject(error.response)
})複製程式碼
定義路由:
const router = new Router({
mode: 'history', routes: [ {
path: '/', name: 'Index', component: Index, meta: {
requiresAuth: true
}
}, {
path: '/login', name: 'Login', component: Login
} ]
})複製程式碼
上面我給首頁路由加了 requiresAuth,所以使用路由鉤子來攔截導航,localStroage 裡有 Token ,就呼叫獲取 userInfo 的方法,並繼續執行,如果沒有 Token ,呼叫退出登入的方法,重定向到登入頁。
router.beforeEach((to, from, next) =>
{
let token = window.localStorage.getItem('token') if (to.meta.requiresAuth) {
if (token) {
store.dispatch('getUser') next()
} else {
store.dispatch('logOut') next({
path: '/login', query: {
redirect: to.fullPath
}
})
}
} else {
next()
}
})複製程式碼
這裡使用了兩個 Vuex 的 action 方法,馬上就會說到 。
Vuex
首先,在 mutation_types 裡定義:
export const LOGIN = 'LOGIN' // 登入export const USERINFO = 'USERINFO' // 使用者資訊export const LOGINSTATUS = 'LOGINSTATUS' // 登入狀態複製程式碼
然後在 mutation 裡使用它們:
const mutations = {
[types.LOGIN]: (state, value) =>
{
state.token = value
}, [types.USERINFO]: (state, info) =>
{
state.userInfo = info
}, [types.LOGINSTATUS]: (state, bool) =>
{
state.loginStatus = bool
}
}複製程式碼
在之前封裝 Axios 的 JS裡定義請求介面:
export const login = ({
loginUser, loginPassword
}) =>
{
return instance.post('/login', {
username: loginUser, password: loginPassword
})
}export const getUserInfo = () =>
{
return instance.get('/profile')
}複製程式碼
在 Vuex 的 actions 裡引入:
import * as types from './types'import {
instance, login, getUserInfo
} from '../api'複製程式碼
定義 action
export default {
toLogin ({
commit
}, info) {
return new Promise((resolve, reject) =>
{
login(info).then(res =>
{
if (res.status === 200) {
commit(types.LOGIN, res.data.token) // 儲存 token commit(types.LOGINSTATUS, true) // 改變登入狀態為 instance.defaults.headers.common['Authorization'] = `Bearer ` + res.data.token // 請求頭新增 token window.localStorage.setItem('token', res.data.token) // 儲存進 localStroage resolve(res)
}
}).catch((error) =>
{
console.log(error) reject(error)
})
})
}, getUser ({
commit
}) {
return new Promise((resolve, reject) =>
{
getUserInfo().then(res =>
{
if (res.status === 200) {
commit(types.USERINFO, res.data) // 把 userInfo 存進 Vuex
}
}).catch((error) =>
{
reject(error)
})
})
}, logOut ({
commit
}) {
// 退出登入 return new Promise((resolve, reject) =>
{
commit(types.USERINFO, null) // 情況 userInfo commit(types.LOGINSTATUS, false) // 登入狀態改為 false commit(types.LOGIN, '') // 清除 token window.localStorage.removeItem('token')
})
}
}複製程式碼
介面
這時候,我們該去寫後端介面了。
我這裡用了阿里的 egg 框架,感覺很強大。
首先定義一個 LoginController :
const Controller = require('egg').Controller;
const jwt = require('jsonwebtoken');
// 引入 jsonwebtokenclass LoginController extends Controller {
async index() {
const ctx = this.ctx;
/* 把使用者資訊加密成 token ,因為沒連線資料庫,所以都是假資料正常應該先判斷使用者名稱及密碼是否正確*/ const token = jwt.sign({
user_id: 1, // user_id user_name: ctx.request.body.username // user_name
}, 'shenzhouhaotian', {
// 祕鑰 expiresIn: '60s' // 過期時間
});
ctx.body = {
// 返回給前端 token: token
};
ctx.status = 200;
// 狀態碼 200
}
}module.exports = LoginController;
複製程式碼
UserController:
class UserController extends Controller {
async index() {
const ctx = this.ctx const authorization = ctx.get('Authorization');
if (authorization === '') {
// 判斷請求頭有沒有攜帶 token ,沒有直接返回 401 ctx.throw(401, 'no token detected in http header "Authorization"');
} const token = authorization.split(' ')[1];
// console.log(token) let tokenContent;
try {
tokenContent = await jwt.verify(token, 'shenzhouhaotian');
//如果 token 過期或驗證失敗,將返回401 console.log(tokenContent) ctx.body = tokenContent // token有效,返回 userInfo ;
同理,其它介面在這裡處理對應邏輯並返回
} catch (err) {
ctx.throw(401, 'invalid token');
}
}
}複製程式碼
在 router.js 裡定義介面:
module.exports = app =>
{
const {
router, controller
} = app;
router.get('/', controller.home.index);
router.get('/profile', controller.user.index);
router.post('/login', controller.login.index);
};
複製程式碼
前端請求
介面寫好了,該前端去請求了。
這裡我寫了個登入元件,下面是點選登入時的 login 方法:
login () {
if (this.username === '') {
this.$message.warning('使用者名稱不能為空哦~~')
} else if (this.password === '') {
this.$message.warning('密碼不能為空哦~~')
} else {
this.$store.dispatch('toLogin', {
// dispatch toLogin action loginUser: this.username, loginPassword: this.password
}).then(() =>
{
this.$store.dispatch('getUser') // dispatch getUserInfo action let redirectUrl = decodeURIComponent(this.$route.query.redirect || '/') console.log(redirectUrl) // 跳轉到指定的路由 this.$router.push({
path: redirectUrl
})
}).catch((error) =>
{
console.log(error.response.data.message)
})
}
}複製程式碼
登入成功後,跳轉到首頁之前重定向過來的頁面。
整體流程跑完了,實現的主要功能就是:
- 訪問登入註冊之外的路由,都需要登入許可權,比如首頁,判斷有無token,有則訪問成功,沒有則跳轉到登入頁面;
- 成功登入之後,跳轉到之前重定向過來的頁面;
- token 過期後,請求介面時,身份過期,跳轉到登入頁,繼續第二步;這一步主要用了可以做7天自動登入等功能。