Koa & Mongoose & Vue實現先後端分離--09身份驗證JWT&測試

上節回顧

  • 嵌套路由
  • 頁面佈局
  • 狀態管理 & 持久化

工做內容

  • 身份認證

準備工做

  • npm i -S crypto-js // 先切換到/server目錄下
  • npm i -S koa-jwt // 先切換到/server目錄下
  • npm i -S jsonwebtoken // 先切換到/server目錄下

業務邏輯

JWT簡介

JWT對象爲一個長字串,字符之間經過"."分隔符分爲三個子串。
JWT的三個部分:JWT頭、有效載荷和簽名。
一旦JWT簽發,在有效期內將會一直有效。前端

  • JWT使用的核心步驟vue

    • 簽名
    • 認證
    • 解碼
  • 這裏使用jsonwebtoken對載荷進行簽名,jsonwebtoken結合koa-jwt進行簽名認證,最終獲得簽名前的數據ctx.state[<key>]

工具庫:簽名

// 新建文件:/server/config/auth.js
const AES = require("crypto-js/aes");
const secretText = 'jwt.secret.text';
const key = 'jwt.secret.key'
// // Encrypt
// var ciphertext = CryptoJS.AES.encrypt('my message', 'secret key 123').toString();

// // Decrypt
// var bytes  = CryptoJS.AES.decrypt(ciphertext, 'secret key 123');
// var originalText = bytes.toString(CryptoJS.enc.Utf8);

// console.log(originalText); // 'my message'
module.exports = {
  secret: AES.encrypt(secretText, key).toString(),
  authKey: 'auth'
}

能夠經過ctx.state[<authKey>],即ctx.state.auth獲取有效荷載。node

// 新建文件:server/utils/auth.js
const jwt = require('jsonwebtoken')
const { secret } = require('../config/auth')
// 定義超時時間:token的有效時長
const expiresIn = '2h';

module.exports = {
  sign: function(payload) {
    // 推薦對payload進行加密。
    const token = jwt.sign(payload, secret, {
      expiresIn
    });

    return token;
  },
  vertify: function(ctx, decodeToken, token){
    
  }
}

工具庫:認證

// 更新文件:server/utils/auth.js
...
  vertify: function(ctx, decodeToken, token){
    let result = true;
    try{
      jwt.verify(token, secret);
      result = false;
    }catch(e) {

    }
    return result;
  }
...

vertify返回true代表token已無效或認證失敗;
vertify返回false代表token仍有效,且已經成功;git

解析

//更新文件:server/app.js
...
const koaJwt = require('koa-jwt');
const { secret, authKey } = require('./config/auth');
const { vertify } = require('./utils/auth');
...
app.use(koaJwt({  //要放到路由前邊,不然,無效
  secret,
  key: authKey,
  // jwt是否被廢除
  isRevoked: vertify
}))
...
  • JWT認證必須放到路由前方,不然,路由邏輯都走完了,再進行認證,有什麼用
  • koa-jwt幫助實現認證邏輯,認證失敗,拋出錯誤。
  • koa-jwt相似koa-body,將有效載荷解析,存儲到ctx.state[<authKey>]ctx.state.auth)中。

服務端登陸返回token

// 更新文件:
...
const auth = require('../utils/auth');
...
async function login (ctx) {
...
  if(user) {
    const token = auth.sign({
      id: user.id,
      account
    })
    ctx.body = {
      code: '200',
      data: {
        token,
        id: user.id,
        account,
        alias: user.alias
      },
      msg: '登錄成功'
    }
  }
...
}
  • Postman測試結果

NotFound

  • vs code 調試控制檯

AuthenticationError

異常處理

上一步認證失敗,拋出了ERROR
monitor
經過斷點能夠查看到錯誤信息,補充異常邏輯
官方推薦經過status判斷是不是認證錯誤
remmandgithub

// 更新文件:server/app.js
···
// 中間件的錯誤處理
app.use(function(ctx, next){
  return next().catch((err) => {
    console.log(err)
    if (err.name === 'ValidationError') {
      ctx.body = {
        code: '403',
        data: null,
        msg: err.message
      }
    } else if (401 == err.status) { //認證錯誤
      ctx.status = 401; // 更新HTTP Response Code
      ctx.body = {
        code: '401',
        data: null,
        msg: `${err.message}\n請先登錄`
      }
    } else {
      throw err;
    }
  });
});
···
  • Postman繼續測試

401

白名單

登陸還須要認證?答案是不須要的。
不單單是登陸,註冊及其後續的靜態圖片訪問,都不須要認證。web

// 更新文件:server/app.js
  secret,
  key: authKey,
  // jwt是否被廢除
  isRevoked: vertify
}).unless({
  // 返回true就是忽略認證
  custom: function(ctx) {
    const { method, path, query } = ctx;
    if(path === '/'){
      return true;
    }
    if(path === '/users' && query.action) {
      return true;
    }
    return false;
  }
}));
  • 鏈式調用unless,返回true即爲忽略認證。
  • unless的具體用法同koa-unless,這裏使用的custom是自定義忽略規則。
  • Postman繼續測試

success
成功,完美。vuex

Postman測試身份認證

然而,用Postman測試其它接口:
Authentication
這是由於沒有在請求頭Headers里加上認證信息Authorization
image.png
在登陸接口的Tests面板,藉助右邊的提示,設置全局變量token爲登陸返回數據的tokennpm

// 更新Tests面板內容:
pm.test("Your test name", function () {
    var jsonData = pm.response.json();
    pm.globals.set("token", jsonData.data.token);
});

image.png
在須要身份認證的測試接口的Authorization面板中,選擇TypeBearer Token,值爲變量{{token}}json

前端請求攔截

到目前爲止,接口沒有什麼問題了,但,前端頁面請求,沒有添加Authorizationapp

// 更新文件:client/src/views/login/index.vue
...
async function onLogin () {
...
        if (res && res.code === '200') {
            const {token, ...user} = res.data // 新增
            localStorage.setItem('token', res.data.token) // 新增
            this.$store.commit('putLoginer', user)
            this.$router.replace('/home')
          }
...
}
...

在登陸時,獲取token,進行本地存儲。

// 更新文件:client/src/utils/http.js
...
instance.interceptors.request.use(async (config) => {
  const token = await localStorage.getItem('token')
  token && (config.headers['Authorization'] = `Bearer ${token}`)
  return config
}, function (error) {
  console.log('------request===========', error)
  // Do something with request error
  return Promise.reject(error)
})
...
  • 攔截請求,token存在時,將token賦值給config.headers['Authorization']
  • 賦值是以Bearer 爲前綴的,這是規範,要求這樣處理。
// 僅爲了測試身份認證,測試完,就把該文件還原。
// 更新文件:client/src/views/homePage/index.vue
...
<script>
import http from '@/utils/http'

export default {
  async created () {
    const res = await http.get('/users')
    console.log(res)
  }
}
</script>
...

測試結果:
auth.gif

前端響應攔截

前端認證失敗,就退回到登陸/頁面,清空本地存儲和vuex。(登出時,也是這些步驟,登出邏輯之前已經處理過了)

// 更新文件:client/src/utils/http.js
...
import router from '@/router'
import store from '@/store'
...
instance.interceptors.response.use(
  async res => {
    if (/^20./.test(res.status)) {
      return res.data
    }
    if (/^40./.test(res.status)) {
      router.push('/')
      await localStorage.clear()
      store.commit('resetVuex')
      return {
        code: '401',
        msg: '請從新登錄'
      }
    }
    console.log('------response=======', res)
    return res
  },
  error => {
    return Promise.reject(error)
  }
)

測試結果以下:退回到登陸/頁面,清空本地存儲和vuex
logout.gif

至於爲何調用兩次/users接口,由於/home下認證一次,/login調用了一次。

這裏改進一下/login的調用,讓若是有token的時候,認證成功,直接進入首頁。(偷懶:專門作一個認證接口比較好)

// 更新文件:client/src/views/login/index.vue
...
  async created () {
    const res = await http.get('/users')
    if (res.code === '200') {
      this.$router.replace('/home')
    }
  }
  ...

參考文檔

koa-jwt
jsonwebtoken

相關文章
相關標籤/搜索