以前博客的認證方式是 session,最近抽了點時間把它改爲了 jwt,順便學習了下 jwt 相關知識。jwt 介紹能夠看阮一峯的文章。html
上圖是最簡單的 jwt 流程,token 過時或者失效後就會跳回登陸頁。不過個人理想狀態應該是一週以內不用登陸,每次登陸以後過時時間都會日後順延一週。在不修改數據庫的前提下,想到如下兩個方法:前端
方案 1 缺點很明顯,就是每次調用接口都要生成一次 token,增長了沒必要要的開銷。而且後端接口每一個都要攜帶 token 信息,後端改動量較大=。=方案 2 好像沒有明顯缺點,選用方案 2 實踐。node
上圖是 refresh token 的認證流程。能夠看到比最簡單的流程多了一步刷新 token 和 refresh token 的步驟,這步也是刷新 token 的關鍵。ios
在項目中約定,401表示 token 超時,402表示 refreshToken 無效
登陸的時候初始化 token 和 refreshTokenweb
// jwt.js const jwt = require('jsonwebtoken') const { SECRET_KEY, JWT_EXPIRES, REFRESH_JWT_EXPIRES } = require('../config/config') // 從配置文件中引入 SECRET_KEY,token 過時時間,refreshToken 過時時間 const { cacheUser } = require('../cache/user') // 用閉包緩存用戶名 /** * 生成 token 和 initToken * @param {string} username */ function initToken (username) { return { token: jwt.sign({ username: username }, SECRET_KEY, { expiresIn: JWT_EXPIRES }), refresh_token: jwt.sign({ username }, SECRET_KEY, { expiresIn: REFRESH_JWT_EXPIRES }) } } /** * 驗證 token/refreshToken * @param {string} token 格式爲 `Beare ${token}` */ function validateToken (token, type) { try { token = token.replace(/^Beare /, '') const { username } = jwt.verify(token, SECRET_KEY) if (type !== 'refreshToken') { // 若是是 token 且 token 生效,緩存 token cacheUser.setUserName(username) } } catch (e) { throw new Error(e.message) } } module.exports = { initToken, getTokenUser, validateToken }
以後在中間件中調用 validateToken
方法數據庫
// 檢查是否登陸中間件方法 checkLogin (req, res, next) { const { headers: { authorization } } = req if (!authorization) { res.status(402).json({ code: 'ERROR', data: '未檢測到登陸信息' }) return false } try { validateToken(authorization) } catch (e) { if (e.message === 'jwt expired') { // token 超時 res.status(401).json({ code: 'ERROR', data: '登陸超時' }) return false } else { res.status(402).json({ code: 'ERROR', data: '未檢測到登陸信息' }) return false } }
在一些須要登陸驗證的接口(好比獲取用戶留言接口),會先調用中間件驗證是否登陸。中間件中會先驗證一次 token。這裏我用閉包寫了一個用戶緩存,中間件驗證成功後將用戶存入閉包,以後在接口中獲取用戶信息時就能夠調用閉包的 getUserName
方法而不用再去解析 token,這樣能夠避免中間件驗證 token 成功但接口中驗證 token 過時帶來的困擾。json
/** * 寫一個閉包來緩存用戶名 */ const cacheUser = (() => { let username = '' return { setUserName: (user) => { if (username === user) { return false } username = user }, getUserName: () => { return username }, clearUserName: () => { username = '' } } })() module.exports = { cacheUser }
在登陸成功後獲取 token 和 refreshToken 返回給前端。axios
js 須要從新封裝 axios 函數segmentfault
import axios from 'axios' import qs from 'qs' ...... // 根據 refreshToken 刷新 token 和 refreshToken const fetchRefreshToken = () => { const token = Cookies.get('refreshToken') return axios({ url: '/api/signin/refreshToken', headers: { Authorization: `Beare ${token}` }, method: 'get' }) } export const post = (url, formData, headers = {}) => { const token = Cookies.get('token') headers = Object.assign({}, headers, { Authorization: `Beare ${token}` }) return axios({ url, headers, method: 'post', data: qs.stringify(formData) }).then(res => { return res }).catch(e => { if (e.response.status === 401) { // token 超時,訪問刷新 token 接口 return fetchRefreshToken().then(res => { const { data } = res.data const { token, refresh_token: refreshToken } = data Cookies.set('token', token) Cookies.set('refreshToken', refreshToken) return post(url, formData, headers) // 從新調用這個接口 }) } }) } ...
修改 axios 攔截函數後端
// axios 攔截器 未登陸則跳轉到登陸頁 axios.interceptors.response.use( res => { if (res.data.code === 'OK') { return res } else { // 提示報錯信息 message.error(res.data.data, 10) return Promise.reject(res) } }, error => { if (error.response) { switch (error.response.status) { case 402: // 登陸超時,跳轉到登陸頁,自行實現 // store.dispatch(actionCreators.logoutSuccess()) break // 跳到登陸頁 default: break } } return Promise.reject(error) } )
目前爲止已經實現了 token 自動刷新的功能。可是這種方法仍是有缺陷的。好比同時調用多個接口時,若是 token 過時了,由於請求是異步的,因此會出現屢次調用 refreshtoken 接口的狀況。這種最優解應該是第一個接口去刷新 token,剩下的先等待,等 token 刷新後剩下的接口才開始調用。
sf 上有一篇文章比較詳細地解釋了這種方法。