node 博客 JWT 驗證明踐

以前博客的認證方式是 session,最近抽了點時間把它改爲了 jwt,順便學習了下 jwt 相關知識。jwt 介紹能夠看阮一峯的文章html

jwt 實現流程

jwt 認證.png

上圖是最簡單的 jwt 流程,token 過時或者失效後就會跳回登陸頁。不過個人理想狀態應該是一週以內不用登陸,每次登陸以後過時時間都會日後順延一週。在不修改數據庫的前提下,想到如下兩個方法:前端

  1. 將 token 的過時時間設爲7天,每次訪問接口時後端根據當前時間從新生成 token,而後在每一個接口都返回新生成的 token,前端接收到後將 token 存入 cookie,並在下一次請求時帶上新的 token。
  2. 登陸後生成一個過時時間爲 3h 的 token 和一個過時時間爲 7 天的 refreshToken。前端請求時在消息頭中攜帶 token 信息,若是 token 過時,前端攜帶 refreshToken 訪問刷新 token 接口,若是 refreshToken 沒有過時則後端返回新的 token 和 refreshToken,不然前端跳轉到登陸頁。前端再根據新返回的 token 去從新訪問剛剛的接口。

方案 1 缺點很明顯,就是每次調用接口都要生成一次 token,增長了沒必要要的開銷。而且後端接口每一個都要攜帶 token 信息,後端改動量較大=。=方案 2 好像沒有明顯缺點,選用方案 2 實踐。node

jwt 認證 refresh token.png

上圖是 refresh token 的認證流程。能夠看到比最簡單的流程多了一步刷新 token 和 refresh token 的步驟,這步也是刷新 token 的關鍵。ios

實現代碼

後端 node

在項目中約定,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

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 上有一篇文章比較詳細地解釋了這種方法。

相關文章
相關標籤/搜索