主要是防止自己之後要重用程式碼時忘記當初做了那些更動,如果你剛好使用相同的框架,那恭喜你!
本篇將會說明如何使用前端(Vue)和後端(flask)實作JWT加上自動refresh方法
會使用到的框架/模組有以下幾種:
前端
後端
前端部分很大幅度參考了這篇文章:
How to auto-refresh jwts using Axios interceptors.
其中為了搭配後端做了一些小改動
首先先使用現成的模組加入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>
注意:要先讀完讀懂這篇文章的內容,這裡只說明為了接後端所做的小改動
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 }; // 讓大家都可以用
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,
},
});
後端部分很大程度參考了
@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.跨域所以有預檢