我的博客的Token驗證明踐(基於Node)

前言

  • 爲何要用 token
HTTP 是一種無狀態的協議,也就是 HTTP 無法保存客戶端的信息,沒辦法區分每次請求的不一樣。

設想這樣的場景,A 和 B 同時修改我的的文章,服務器同時收到兩個 post 請求,但瀏覽器並不知道哪一個請求是 A 哪一個請求是 B,須要一個標識符(token)來標記一串信息而且在請求的時候帶上前端

  • token 是什麼
Token 是服務器生成的一串字符,做爲客戶端請求的令牌。當第一次登錄後,服務器會分發 Tonken 字符串給客戶端。後續的請求,客戶端只需帶上這個 Token,服務器便可知道是該用戶的訪問。

我的理解就是一串被服務器加密過的我的信息,好比下面這個:vue

acess_token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI1ZWRhMTBjOTI0NThmNDAwMmFjZDEyMTAiLCJuaWNrX25hbWUiOiLlsI_osaoxOTk2IiwiY3JlYXRlZF90aW1lIjoiMjAyMC0wNi0wNVQwOTozMDo0OS42MThaIiwidXBkYXRlZF90aW1lIjoiMjAyMC0wNi0wNVQwOToyOToyMC4wNzlaIiwiaWF0IjoxNTkxMzQ5NDY4LCJleHAiOjE1OTE5NTQyNjh9.GmUJRXHed7M1xJyPaFFgaQKJoS-w8-l3N_PQFPiwwTE

服務器經過祕鑰解密從而得到當前請求者的信息node

技術棧

  • 前端:vue + ssr
  • 後端:egg(一個 node 框架) + ts
  • 數據庫:redis + mongo
  • 部署:Docker
  • 構建:Jenkins

blog 已經基本完成,而且用上了 ssr 渲染ios

本文主要是講 token 驗證,其餘再也不累述git

token 驗證設計

個人 blog 對 get 類型的請求不作 token 驗證,其餘會修改資源的請求如 POST、PUT 會作 token 驗證github

可拆解爲下面 3 個步驟web

  • 客戶端用戶登陸,服務端根據用戶信息生成 token 並在客戶端持久化存儲
  • 客戶端請求,帶上 token
  • 服務端驗證 token,若失敗則直接返回錯誤狀態

1.前端(vue.js)redis

使用 axios 庫,而且在 request 攔截器中把 token 塞到請求頭 header ,在 response 攔截器中統一對錯誤狀態碼進行全局提示數據庫

登陸成功以後把 token 寫入 瀏覽器緩存 中,每次請求都帶上json

import Vue from 'vue';
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';

axios.interceptors.request.use(
  async (config: AxiosRequestConfig) => {
    const acess_token = await Vue.prototype.$getCacheData('acess_token'); # 緩存中讀取token
    if (acess_token) {
      config.headers.acess_token = acess_token;
    }
    return config;
  },
  (err: any) => Promise.reject(err),
);

axios.interceptors.response.use(
  (response: AxiosResponse) => {
    if (response.data.ret === 200) {
      return response;
    } else {
      Vue.prototype.$global_fail(response.data.content);
      return Promise.reject(response);
    }
  },
  (err: any) => {
    console.log(err);
    if (err.code === 'ECONNABORTED' && err.message.indexOf('timeout') !== -1) {
      Vue.prototype.$global_error('請求超時,請聯繫管理員');
    }
    if (err.response) {
      Vue.prototype.$global_error(decodeURI(err.response.data.msg || err.response.data.message));
    }
    return Promise.reject(err);
  },
);

關於前端緩存,這裏我推薦一個 localForage 庫,很實用

localForage 是一個 JavaScript 庫,能存儲多種類型的數據,而不單單是字符串。localForage 有一個優雅降級策略,若瀏覽器不支持 IndexedDB 或 WebSQL,則使用 localStorage。

但注意,它的操做都是異步的,能夠本身封裝一層把它改爲同步的

import Vue from 'vue';
import localForage from 'localforage';

Vue.prototype.$setCacheData = async (key: string, data: any): Promise<void> => await localForage.setItem(key, data);
Vue.prototype.$getCacheData = async (key: string): Promise<string | null> => await localForage.getItem(key) || null;
Vue.prototype.$clearCache = () => localForage.clear();

2.後端(egg.js)

  • 生成 token

用戶登陸,使用 jsonwebtoken 生成 token

jsonwebtoken 的詳情請點擊: node-jsonwebtoken

加密解密使用也比較簡單,直接給出 UserService 方法,其中 secret 爲祕鑰,且能夠設置 token 過時時間

# /app/service/user.ts
import * as jwt from 'jsonwebtoken';

export default class UserService extends Service {

  private secret = 'Hello__World'; # 祕鑰
  async createToken(user: User): Promise<string> {
    const payload = {
      _id: user._id,
      nick_name: user.nick_name,
      created_time: user.created_time,
      updated_time: user.updated_time,
    };
    return jwt.sign(payload, this.secret, { expiresIn: '7d' }); # 過時時間
  }

  checkToken(token: string): User {
    try {
      # 根據祕鑰解密token
      return jwt.verify(token, this.secret);
    } catch (e) {
      throw '無效的token';
    }
  }

}

在中間件中進行 token 驗證,若失敗直接返回

  • 中間件驗證

開啓 verify 中間件,並只對特定的 POST 請求進行驗證:

開啓中間件

# /config/config.default.ts
config.middleware = ['verify'];
config.verify = {
  enable: true,
  # 只對POST請求作驗證
  match(ctx) {
    return ctx.request.method === 'POST';
  },
};

在 verify 調用 checkToken 方法驗證 token

# /app/middleware/verify.ts
module.exports = () => {
  return async (ctx, next) => {
    if (ctx.path.startsWith('/api/user/login') || ctx.path.startsWith('/api/user/sendCode') || ctx.path.startsWith('/api/user/register')) {
      return await next();
    }
    try {
      const acess_token: string = ctx.request.header.acess_token;
      if (!acess_token) {
        throw '請登陸';
      } else {
        await ctx.service.user.checkToken(acess_token); # 驗證token
        return await next();
      }
    } catch (e) {
      # token驗證失敗會走到這裏,返回自定義狀態碼
      console.log(e);
      ctx.body = {
        ret: 304,
        content: `${e}`,
      };
    }
  };
};

至此 token 驗證就完了,若有不足,歡迎指出


END

相關文章
相關標籤/搜索