這篇文章寫一下前后端分離下的登錄解決方案,目前大多數都采用請求頭攜帶 Token 的形式。
開寫之前先捋一下整理思路:
- 首次登錄時,后端服務器判斷用戶賬號密碼正確之后,根據用戶id、用戶名、定義好的秘鑰、過期時間生成 token ,返回給前端;
- 前端拿到后端返回的 token ,存儲在 localStroage 和 vuex 里;
- 前端每次路由跳轉,判斷 localStroage 有無 token ,沒有則跳轉到登錄頁,有則請求獲取用戶信息,改變登錄狀態;
- 每次請求接口,在 Axios 請求頭里攜帶 token;
- 后端接口判斷請求頭有無 token,沒有或者 token 過期,返回401;
- 前端得到 401 狀態碼,重定向到登錄頁面。
這里前端使用 vue ,后端使用阿里的 egg
首先,我們先輕微封裝一下 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'); // 引入 jsonwebtoken class 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); };
資源搜索網站大全 https://www.renrenfan.com.cn
前端請求
接口寫好了,該前端去請求了。
這里我寫了個登錄組件,下面是點擊登錄時的 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天自動登錄等功能。