token響應式設定

adlixiaxia發表於2020-12-02

處理使用者 Token
在這裡插入圖片描述
Token 是使用者登入成功之後服務端返回的一個身份令牌,在專案中的多個業務中需要使用到:

  • 訪問需要授權的 API 介面
  • 校驗頁面的訪問許可權

但是我們只有在第一次使用者登入成功之後才能拿到 Token。

所以為了能在其它模組中獲取到 Token 資料,我們需要把它儲存到一個公共的位置,方便隨時取用。

往哪兒存?

  • 本地儲存
    • 獲取麻煩
    • 資料不是響應式
  • Vuex 容器(推薦)
    • 獲取方便
    • 響應式的

在這裡插入圖片描述

  • 登入成功,將 Token 儲存到 Vuex 容器中
    • 獲取方便
    • 響應式
  • 為了持久化,還需要把 Token 放到本地儲存
    • 持久化
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    // 使用者的登入狀態資訊
    user: JSON.parse(window.localStorage.getItem('TOUTIAO_USER'))
    // user: null
  },
  mutations: {
    setUser (state, user) {
      state.user = user
      window.localStorage.setItem('TOUTIAO_USER', JSON.stringify(user))
    }
  },
  actions: {
  },
  modules: {
  }
})

2、登入成功以後將後端返回的 token 相關資料儲存到容器中

async onLogin () {
  // const loginToast = this.$toast.loading({
  this.$toast.loading({
    duration: 0, // 持續時間,0表示持續展示不停止
    forbidClick: true, // 是否禁止背景點選
    message: '登入中...' // 提示訊息
  })

  try {
    const res = await login(this.user)

    // res.data.data => { token: 'xxx', refresh_token: 'xxx' }
+    this.$store.commit('setUser', res.data.data)

    // 提示 success 或者 fail 的時候,會先把其它的 toast 先清除
    this.$toast.success('登入成功')
  } catch (err) {
    console.log('登入失敗', err)
    this.$toast.fail('登入失敗,手機號或驗證碼錯誤')
  }

  // 停止 loading,它會把當前頁面中所有的 toast 都給清除
  // loginToast.clear()
}

關於 Token 過期問題

登入成功之後後端會返回兩個 Token:

  • token:訪問令牌,有效期2小時
  • refresh_token:重新整理令牌,有效期14天,用於訪問令牌過期之後重新獲取新的訪問令牌

我們的專案介面中設定的 Token 有效期是 2 小時,超過有效期服務端會返回 401 表示 Token 無效或過期了。

為什麼過期時間這麼短?

  • 為了安全,例如 Token 被別人盜用

過期了怎麼辦?

  • 讓使用者重新登入,使用者體驗太差了
  • 使用 refresh_token 解決 token 過期

如何使用 refresh_token 解決 token 過期?
處理邏輯
處理流程:

  1. 在axios的攔截器中加入token重新整理邏輯
  2. 當使用者token過期時,去向伺服器請求新的 token
  3. 把舊的token替換為新的token
  4. 然後繼續使用者當前的請求

在請求的響應攔截器中統一處理 token 過期:

 * 封裝 axios 請求模組
 */
import axios from "axios";
import jsonBig from "json-bigint";
import store from "@/store";
import router from "@/router";

// axios.create 方法:複製一個 axios
const request = axios.create({
  baseURL: "http://ttapi.research.itcast.cn/" // 基礎路徑
});

/**
 * 配置處理後端返回資料中超出 js 安全整數範圍問題
 */
request.defaults.transformResponse = [
  function(data) {
    try {
      return jsonBig.parse(data);
    } catch (err) {
      return {};
    }
  }
];

// 請求攔截器
request.interceptors.request.use(
  function(config) {
    const user = store.state.user;
    if (user) {
      config.headers.Authorization = `Bearer ${user.token}`;
    }
    // Do something before request is sent
    return config;
  },
  function(error) {
    // Do something with request error
    return Promise.reject(error);
  }
);

// 響應攔截器
request.interceptors.response.use(
  // 響應成功進入第1個函式
  // 該函式的引數是響應物件
  function(response) {
    // Any status code that lie within the range of 2xx cause this function to trigger
    // Do something with response data
    return response;
  },
  // 響應失敗進入第2個函式,該函式的引數是錯誤物件
  async function(error) {
    // Any status codes that falls outside the range of 2xx cause this function to trigger
    // Do something with response error
    // 如果響應碼是 401 ,則請求獲取新的 token

    // 響應攔截器中的 error 就是那個響應的錯誤物件
    console.dir(error);
    if (error.response && error.response.status === 401) {
      // 校驗是否有 refresh_token
      const user = store.state.user;

      if (!user || !user.refresh_token) {
        router.push("/login");

        // 程式碼不要往後執行了
        return;
      }

      // 如果有refresh_token,則請求獲取新的 token
      try {
        const res = await axios({
          method: "PUT",
          url: "http://ttapi.research.itcast.cn/app/v1_0/authorizations",
          headers: {
            Authorization: `Bearer ${user.refresh_token}`
          }
        });

        // 如果獲取成功,則把新的 token 更新到容器中
        console.log("重新整理 token  成功", res);
        store.commit("setUser", {
          token: res.data.data.token, // 最新獲取的可用 token
          refresh_token: user.refresh_token // 還是原來的 refresh_token
        });

        // 把之前失敗的使用者請求繼續發出去
        // config 是一個物件,其中包含本次失敗請求相關的那些配置資訊,例如 url、method 都有
        // return 把 request 的請求結果繼續返回給發請求的具體位置
        return request(error.config);
      } catch (err) {
        // 如果獲取失敗,直接跳轉 登入頁
        console.log("請求刷線 token 失敗", err);
        router.push("/login");
      }
    }

    return Promise.reject(error);
  }
);

export default request;

相關文章