--- tags: more than 3min lang: zh-tw --- # 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.](https://lewiskori.com/blog/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攔截器 **注意:要先讀完讀懂[這篇文章](https://lewiskori.com/blog/how-to-auto-refresh-jwts-using-axios-interceptors/)的內容,這裡只說明為了接後端所做的小改動** 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](https://flask-jwt-extended.readthedocs.io/en/stable/refreshing_tokens/#explicit-refreshing-with-refresh-tokens) ``` @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.跨域所以有預檢 ![](https://i.imgur.com/9R22W27.jpg)