Try   HackMD

Vue.js+flask實作JWT with auto-refresh

主要是防止自己之後要重用程式碼時忘記當初做了那些更動,如果你剛好使用相同的框架,那恭喜你!

概要

本篇將會說明如何使用前端(Vue)和後端(flask)實作JWT加上自動refresh方法
會使用到的框架/模組有以下幾種:

前端

  • Vue
    • vue-google-login (可有可無,Google登入)
    • axios (連接後端API)
    • vuex (存token)
    • vuex-persistedstate (存token)
    • vue-router (可有可無)

後端

  • python flask
    • flask_jwt_extended (生成token+驗證+refresh)

前端實作

前端部分很大幅度參考了這篇文章:

How to auto-refresh jwts using Axios interceptors.

其中為了搭配後端做了一些小改動

Google Sign In

首先先使用現成的模組加入Google Sign-In按鈕
之所以會另外用模組是因為google文件中的教學用在webpack上要import時似乎會有點小問題
我們就爽用其他人做好的輪子吧

npm install vue-google-login

使用十分簡單
src\components\SignIn.vue template部分內容

<GoogleLogin
      :params="params"
      :renderParams="renderParams"
      :onSuccess="onSuccess"
      :onFailure="onFailure"
    ></GoogleLogin>

src\components\SignIn.vue script內容

<script>
import { googleSignInAPI } from "../api.js";
import GoogleLogin from "vue-google-login";
export default {
  name: "SignIn",
  mounted() {},
  created() {},
  components: {
    GoogleLogin,
  },
  data: () => ({
    // client_id是你申請Google Oauth的用戶端ID
    params: {
      client_id:
        "changethisxxxxxxx.apps.googleusercontent.com",
    },
    // renderParams可讓你自行定義按鈕外觀,詳見google doc
    renderParams: {
      width: 250,
      height: 50,
      longtitle: true,
    },
  }),
  methods: {
    onSuccess(googleUser) {
      var id_token = googleUser.getAuthResponse().id_token;
      var profile = googleUser.getBasicProfile();
      console.log("ID: " + profile.getId()); // Do not send to your backend! Use an ID token instead.
      console.log("Name: " + profile.getName());
      console.log("Image URL: " + profile.getImageUrl());
      console.log("Email: " + profile.getEmail()); // This is null if the 'email' scope is not present.
      this.$store.dispatch("logIn", { token: id_token });//這行接後端API用,之後會定義到
    },
    onFailure() {},
  },
};
</script>

開始實作-axios攔截器

注意:要先讀完讀懂這篇文章的內容,這裡只說明為了接後端所做的小改動

src\helpers\axios.js的部分我們不要把攔截器包成axiosSetUp()作用在全域
而是創建一個新的axios實例,對他進行改動以及加入攔截器,之後需要用到此功能時再拿來用

src\helpers\axios.js 修改後內容

import axios from "axios";
import store from "../store";
import router from "../router";

const axiosRefreshInstance = axios.create();
//這個實例用來refresh token,之後會提到
axiosRefreshInstance.defaults.baseURL =
    "https://app.apiendpoint/";


const axiosApiInstance = axios.create();
//這個才是用來發送攜帶JWT的實例
// point to your API endpoint
axiosApiInstance.defaults.baseURL =
    "https://app.apiendpoint/";
// Add a request interceptor
axiosApiInstance.interceptors.request.use(
    function(config) {
        // Do something before request is sent
        const token = store.getters.accessToken;
        if (token) {
            config.headers.Authorization = `Bearer ${token}`;
        }
        return config;
    },
    function(error) {
        // Do something with request error
        return Promise.reject(error);
    }
);


// Add a response interceptor
axiosApiInstance.interceptors.response.use(
    function(response) {
        // Any status code that lie within the range of 2xx cause this function to trigger
        // Do something with response data
        return response;
    },
    async function(error) {
        // Any status codes that falls outside the range of 2xx cause this function to trigger
        // Do something with response error
        const originalRequest = error.config;
        if (
            error.response.status === 401 &&
            originalRequest.url.includes("auth/refresh/")
        ) {
            store.commit("clearUserData");
            router.push("/signin");
            return Promise.reject(error);
        } else if (error.response.status === 401 && !originalRequest._retry) {
            originalRequest._retry = true;
            await store.dispatch("refreshToken");
            return axiosApiInstance(originalRequest);
        }
        return Promise.reject(error);
    }
);

export { axiosApiInstance, axiosRefreshInstance }; // 讓大家都可以用

開始實作-store actions

src\store\index.js裡的內容基本上都一樣,要改的只有把axios改成axiosApiInstance這個我們之前設定好攔截器的實例

再來還有將refreshToken這個action修改成符合方便呼叫後端API的結構
因為我們的後端收到refresh這個POST的時候是直接從header中原本攜帶JWT的欄位(Authorization)種取得token並視做refresh token去做處理
文章中後端似乎是從payload中去取得refresh token

import Vue from "vue";
import Vuex from "vuex";
import createPersistedState from "vuex-persistedstate";
import router from "../router";
import axios from "axios";
import { axiosApiInstance, axiosRefreshInstance } from "../helpers/axios";

Vue.use(Vuex);

export default new Vuex.Store({
    plugins: [createPersistedState()],
    state: {
        refresh_token: "",
        access_token: "",
        loggedInUser: {},
        isAuthenticated: false,
    },
    mutations: {
        setRefreshToken: function(state, refreshToken) {
            state.refresh_token = refreshToken;
        },
        setAccessToken: function(state, accessToken) {
            state.access_token = accessToken;
        },
        // sets state with user information and toggles
        // isAuthenticated from false to true
        setLoggedInUser: function(state, user) {
            state.loggedInUser = user;
            state.isAuthenticated = true;
        },
        // delete all auth and user information from the state
        clearUserData: function(state) {
            state.refresh_token = "";
            state.access_token = "";
            state.loggedInUser = {};
            state.isAuthenticated = false;
        },
    },
    actions: {
        logIn: async ({ commit, dispatch }, payload) => {
            const loginUrl = "auth/signin";
            try {
                await axiosApiInstance
                    .post(loginUrl, payload)
                    .then((response) => {
                        if (response.status === 200) {
                            commit(
                                "setRefreshToken",
                                response.data.refresh_token
                            );
                            commit(
                                "setAccessToken",
                                response.data.access_token
                            );
                            dispatch("fetchUser");
                            // redirect to the home page
                            router.push({
                                name: "Home",
                                params: { new_user: response.data.new_user },
                            });
                        }
                    });
            } catch (e) {
                console.log(e);
            }
        },
        refreshToken: async ({ state, commit }) => {
            const refreshUrl = "auth/refresh";
            try {
            // 使用剛剛定義的axiosRefreshInstance(沒有攔截器但有預設baseUrl)來呼叫API
            // 並將refresh token放到Authorization欄位
                await axiosRefreshInstance
                    .post(
                        refreshUrl,
                        {},
                        {
                            headers: {
                                Authorization: "Bearer " + state.refresh_token,
                            },
                        }
                    )
                    .then((response) => {
                        if (response.status === 200) {
                            commit(
                                "setAccessToken",
                                response.data.access_token
                            );
                        }
                    });
            } catch (e) {
                console.log(e.response);
            }
        },
        fetchUser: async ({ commit }) => {
            const currentUserUrl = "auth/me";
            try {
                await axiosApiInstance.get(currentUserUrl).then((response) => {
                    if (response.status === 200) {
                        commit("setLoggedInUser", response.data.user_id);
                    }
                });
            } catch (e) {
                console.log(e.response);
            }
        },
    },
    getters: {
        loggedInUser: (state) => state.loggedInUser,
        isAuthenticated: (state) => state.isAuthenticated,
        accessToken: (state) => state.access_token,
        refreshToken: (state) => state.refresh_token,
    },
});

後端實作

後端部分很大程度參考了

flask-jwt-extended doc

@app.route("/auth/refresh", methods=["POST"])
@jwt_required(refresh=True)
def refresh():
    identity = get_jwt_identity()
    access_token = create_access_token(identity=identity)
    return jsonify(access_token=access_token), 200

結束w

結果

ps.跨域所以有預檢