如何使用 Axios 拦截器自动刷新 JWT
我在之前的文章中已经介绍过 JWT 身份验证。为了方便大家快速回顾,我将简要介绍一下 JWT 是什么。
什么是 JWT?
JSON Web Token (JWT) 是一种互联网标准,用于创建基于 JSON 的访问令牌,该令牌包含一系列声明。例如,服务器可以生成一个带有“以管理员身份登录”或“以该用户身份登录”标志的令牌,并将其提供给客户端。客户端随后可以使用该令牌来证明其已以管理员身份登录。令牌由一方(通常是服务器)的私钥签名,以便双方都能验证令牌的合法性。令牌的设计目标是简洁、URL 安全,尤其适用于 Web 浏览器单点登录 (SSO) 环境。JWT 声明通常用于在身份提供商和服务提供商之间传递已认证用户的身份。
与基于令牌的身份验证不同,JWT 不会存储在应用程序的数据库中。这实际上使它们成为无状态的。
JWT 身份验证通常涉及两个令牌:访问令牌和刷新令牌。访问令牌用于验证对 API 的 HTTP 请求,对于受保护的资源,必须在请求头中提供访问令牌。
为了增强安全性,访问令牌通常有效期较短,因此为了避免用户或应用程序每隔几分钟就登录一次,刷新令牌提供了一种获取新访问令牌的方法。刷新令牌的有效期通常比访问令牌长。
在我之前的文章中,我使用Django 实现了 JWT 身份验证,但这在大多数后端框架中都可以实现。
在本教程中,我们将使用 Axios,这是一个流行的基于 Promise 的 JavaScript HTTP 客户端,用于执行 HTTP 通信。它有一个强大的功能,称为拦截器。拦截器允许您在请求/响应到达最终目的地之前对其进行修改。
我们将使用 Vuex 进行全局状态管理,但您也可以轻松地在您选择的任何 JavaScript 框架或方法中实现配置。
项目初始化
由于这是一个 Vue 项目,我们首先需要初始化一个 Vue 项目。更多信息请查看 vue.js 安装指南。
vue create interceptor
项目初始化完成后,我们需要安装 Vuex 和一个名为vuex-persistedstate的实用库。这样,即使浏览器标签页刷新导致 store 数据被清除,我们的状态也会持久化到本地存储中。
yarn add vuex vuex-persistedstate
店铺设置
要初始化 Vuex store,我们需要在src目录中创建一个名为 store 的文件夹。在 store 文件夹中,创建一个名为 index.js 的文件,并填充以下内容。
import Vue from "vue";
import Vuex from "vuex";
import createPersistedState from "vuex-persistedstate";
import router from "../router"; // our vue router instance
Vue.use(Vuex);
export default new Vuex.Store({
plugins: [createPersistedState()],
state: {},
mutations: {},
actions: {},
getters: {}
});
我们暂时先这样,稍后再填充各个部分。现在,我们先在 main.js 文件中注册这个 store。
import Vue from "vue";
import App from "./App.vue";
import store from "./store";
new Vue({
store,
render: h => h(App)
}).$mount("#app");
状态和突变
在 Vuex store 中,唯一真正改变状态的方法是提交 mutation。Vuex mutation 与事件非常相似:每个 mutation 都有一个字符串类型和一个处理函数。处理函数用于执行实际的状态修改,它会接收状态作为第一个参数。
我们的应用程序将包含一些状态对象和变更操作。
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;
}
},
目前为止,代码本身很容易理解,mutations 会用相关信息更新我们的状态值,但这些数据从何而来呢?这就需要用到 actions 了。
Vuex Actions
行为与突变类似,区别在于:
- 动作不会改变状态,而是会造成状态改变。
- 动作可以包含任意异步操作。
这意味着操作会调用变更方法,从而更新状态。操作也可以是异步的,这允许我们进行后端 API 调用。
actions: {
logIn: async ({ commit, dispatch }, payload) => {
const loginUrl = "v1/auth/jwt/create/";
try {
await axios.post(loginUrl, payload).then(response => {
if (response.status === 200) {
commit("setRefreshToken", response.data.refresh);
commit("setAccessToken", response.data.access);
dispatch("fetchUser");
// redirect to the home page
router.push({ name: "home" });
}
});
} catch (e) {
console.log(e);
}
},
refreshToken: async ({ state, commit }) => {
const refreshUrl = "v1/auth/jwt/refresh/";
try {
await axios
.post(refreshUrl, { refresh: state.refresh_token })
.then(response => {
if (response.status === 200) {
commit("setAccessToken", response.data.access);
}
});
} catch (e) {
console.log(e.response);
}
},
fetchUser: async ({ commit }) => {
const currentUserUrl = "v1/auth/users/me/";
try {
await axios.get(currentUserUrl).then(response => {
if (response.status === 200) {
commit("setLoggedInUser", response.data);
}
});
} catch (e) {
console.log(e.response);
}
}
},
我们将逐一讲解这些方法。
登录函数顾名思义,会调用后端 JWT 创建端点。我们预期响应会包含刷新令牌和访问令牌。
具体情况取决于你的实现,所以请相应地实现该方法。
接下来,我们会调用 mutation 来将访问令牌和刷新令牌设置到状态中。如果设置成功,我们会使用 dispatch 关键字来调用fetchUseraction。这是在 Vuex 中调用 action 的一种方式。
它refreshToken会向我们的后端发送一个包含当前刷新令牌的 HTTP POST 请求,如果有效,则会收到一个新的访问令牌,然后该令牌会替换已过期的令牌。
获取器
最后,我们将通过 getter 方法公开状态数据,以便于引用这些数据。
getters: {
loggedInUser: state => state.loggedInUser,
isAuthenticated: state => state.isAuthenticated,
accessToken: state => state.access_token,
refreshToken: state => state.refresh_token
}
Axios拦截器
目前一切顺利。最难的部分已经完成了!
为了设置拦截器,我们将在 src 目录中创建一个名为 helpers 的文件夹,并创建一个名为 的文件。axios.js
这将包含以下代码。
import axios from "axios";
import store from "../store";
import router from "../router";
export default function axiosSetUp() {
// point to your API endpoint
axios.defaults.baseURL = "http://127.0.0.1:8000/api/";
// Add a request interceptor
axios.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
axios.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/jwt/refresh/")
) {
store.commit("clearUserData");
router.push("/login");
return Promise.reject(error);
} else if (error.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
await store.dispatch("refreshToken");
return axios(originalRequest);
}
return Promise.reject(error);
}
);
}
从上面的代码中,我们将导入 axios 并在axiosSetup方法内部对其进行配置。首先,我们需要声明此 axios 实例的 baseURL。您可以将其指向后端 URL。此配置将简化 API 调用,因为我们无需在每个 HTTP 请求中显式输入完整的 URL。
请求拦截器
我们的第一个拦截器是请求拦截器。我们会修改来自前端的每个请求,在请求中添加授权标头。我们将在这里使用访问令牌。
// Add a request interceptor
axios.interceptors.request.use(
function(config) {
// Do something before request is sent
// use getters to retrieve the access token from vuex
// store
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);
}
);
我们正在检查存储中是否存在访问令牌,如果存在,则修改授权标头,以便在每个请求中使用该令牌。
如果令牌不存在,则标头中将不包含授权密钥。
响应拦截器
我们将提取本节的axios 配置。请查阅其文档以了解更多详情。
// Add a response interceptor
axios.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;
},
// remember to make this async as the store action will
// need to be awaited
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/jwt/refresh/")
) {
store.commit("clearUserData");
router.push("/login");
return Promise.reject(error);
} else if (error.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
// await execution of the store async action before
// return
await store.dispatch("refreshToken");
return axios(originalRequest);
}
return Promise.reject(error);
}
);
我们在响应拦截器中设置了两个回调函数。一个在收到 HTTP 请求的响应时执行,另一个在发生错误时执行。
如果没有错误,我们将直接返回响应。如果有错误,我们将进行相应的处理。
第一个 if 语句检查请求是否收到 401(未授权)错误,这表示我们尝试向后端传递无效凭据时发生的情况;同时检查原始请求的 URL 是否指向刷新端点。
如果是,则表示刷新令牌也已过期,因此我们将注销用户并清除其存储数据。然后,我们将用户重定向到登录页面以获取新的访问凭据。
在第二个代码块(else if)中,我们将再次检查请求是否失败并返回状态码 401(未授权),以及这次是否再次失败。
如果不是重试,我们将分发refreshToken操作并重试原始的 HTTP 请求。
最后,对于所有其他状态不在 2xx 范围内的失败请求,我们将返回被拒绝的 Promise,该 Promise 可以在我们应用程序的其他地方进行处理。
在我们的 Vue 应用中实现 axios 的全球可用性
拦截器都设置好了,接下来我们需要一种方法来访问 axios 并利用这些强大的功能!
为此,我们需要axiosSetup在 main.js 文件中导入相应的方法。
import Vue from "vue";
import App from "./App.vue";
import store from "./store";
import axiosSetup from "./helpers/interceptors";
// call the axios setup method here
axiosSetup()
new Vue({
store,
render: h => h(App)
}).$mount("#app");
搞定了!!我们已经设置好了 Axios 拦截器,它们在我们的应用程序中全局可用。无论是在组件中还是在 Vuex 中,每次 Axios 调用都会执行它们!
希望这些内容对您有所帮助!
如有任何疑问,欢迎留言。我的推特私信一直开放。如果您喜欢这篇攻略,请订阅我的邮件列表,以便在我发布新文章时收到通知。
乐于合作
我最近在我的网站上新增了一个合作页面。如果您有有趣的合作项目构想,或者想寻找兼职人员,
现在可以直接通过我的网站预约合作。