前後端分離:使用 token 登入解決方案
阿新 • • 發佈:2020-11-28
這篇文章寫一下前後端分離下的登入解決方案,目前大多數都採用請求頭攜帶 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天自動登入等功能。