Security in front-end applications differ based on requirements and as an engineer, your task is to meet those requirements while keeping a remarkable experience for your users. Using the Bearer Token authentication mechanism you would notice some differences in the approach when designing some applications. Financial apps require constant authentication and re-authentication to protect access to your money, but for less serious apps like educational apps and apps for media consumption it's going to be a bad experience for your users to be reauthenticating themselves anytime they come back to your app. Before you proceed with this article there are certain things you should be aware of:
前端应用程序中的安全性根据要求而有所不同,作为工程师,您的任务是要满足这些要求,同时为用户保留出色的体验。 使用Bearer Token身份验证机制,您会在设计某些应用程序时注意到方法上的一些差异。 金融应用程序需要不断进行身份验证和重新身份验证,以保护对您的资金的访问权限,但是对于不那么严肃的应用程序(例如教育应用程序和用于媒体消费的应用程序),用户每次返回到您的应用程序时都要重新进行身份验证将是一种糟糕的体验。 在继续本文之前,您应该注意以下几点:
- This article only covers the Bearer Token method of authentication in building applications. 本文仅介绍构建应用程序中身份验证的Bearer Token方法。
- This article assumes you have adequate knowledge of authentication using the Bearer Token method. 本文假定您具有使用Bearer Token方法进行身份验证的足够知识。
- This article assumes you have proper knowledge of Axios (Promise based HTTP client for the browser and node.js). 本文假定您具有Axios(用于浏览器和node.js的基于Promise的HTTP客户端)的适当知识。
- This article assumes you are using Axios for your HTTP client as using other clients may differ. 本文假定您将Axios用于HTTP客户端,因为使用其他客户端可能有所不同。
- This article does not cover the back-end aspect of a refresh-token functionality. It assumes the API’s are ready for the refresh-token and is supposed to guide you on how to go about implementing the front-end aspect. 本文不介绍刷新令牌功能的后端方面。 它假定API已准备好进行刷新令牌,并且将指导您如何实现前端方面。
THE PROBLEM
问题
The defacto way for building applications is to authenticate your users then when the backend invalidates their token you throw them back at a log-in page which works but it may break the flow of usage for the application. Imagine you scrolling through your Instagram and you were to leave it for say a day and the moment you revisit it, it slams you back at a log-in page. It is not rocket science to discover when such an experience becomes a bottleneck and a suitable solution to this is to implement a refresh-token functionality for your application whereas when the token has expired quickly request for a new one. In order to fully implement this solution we need to come up with answers to the following questions:
构建应用程序的实际方法是对用户进行身份验证,然后在后端使他们的令牌无效时,您将他们扔回到登录页面,该页面可以运行,但可能会中断应用程序的使用流程。 想象一下,您在Instagram上滚动浏览时,要留出一天的时间,并在重新访问它的那一刻,它让您回到登录页面。 发现这种经历何时成为瓶颈,而对此的合适解决方案是为您的应用程序实现刷新令牌功能,而当令牌过期后,快速请求新的令牌解决方案并不是火箭科学。 为了完全实施此解决方案,我们需要提出以下问题的答案:
- How would a refresh token work? 刷新令牌将如何工作?
- What data do we need to implement a refresh-token solution? 我们需要什么数据来实现刷新令牌解决方案?
- Where/how would we store/read our refresh-token related data? 我们将在哪里/如何存储/读取与刷新令牌相关的数据?
- How do we effectively implement the refresh token functionality with axios? 我们如何使用axios有效地实现刷新令牌功能?
- How do we effectively implement the refresh token functionality with our router? 我们如何通过路由器有效地实现刷新令牌功能?
HOW WOULD A REFRESH TOKEN WORK?
如何刷新令牌工作?
This step is basically the planning stage. This is where we mind-map the entire process we would be implementing in code. Since we cannot tell for sure what page or activity a user would be at when their token expires while using our application, We need to determine the points to hook our refresh token functionality, they are:
此步骤基本上是计划阶段。 在这里,我们可以对将要在代码中实现的整个过程进行映射。 由于我们无法确定使用该应用程序时用户令牌到期时用户将进入哪个页面或活动,因此我们需要确定挂钩刷新令牌功能的要点,它们是:
1. HTTP Client: The HTTP client is a perfect point to hook our refresh token solution. This is because our HTTP client would be responsible for sending requests to our backend service and returning the responses. It is a no brainer to choose this point as this is the first point of detection of an expired token, as when you place a request with an expired token the backend would respond with an authentication error and it is at this point we can effectively request for a token refresh.
1. HTTP客户端:HTTP客户端是连接我们的刷新令牌解决方案的理想之地。 这是因为我们的HTTP客户端将负责向我们的后端服务发送请求并返回响应。 毫不费力地选择这一点,因为这是检测到过期令牌的第一点,因为当您发出带有过期令牌的请求时,后端将响应身份验证错误,这时我们可以有效地请求进行令牌刷新。
2. Router Instance: Most Frontend frameworks provide us with a router/routing instance to navigate users through our application. These routing instances usually support hooks based on various navigation events/stages like before a page load or on page enter and so on. The router instance would be an excellent point to hook our refresh token solution as when a user is transitioning between pages or when the application is rehydrated from a minimized browser tab, we can run a quick check on the validity of our current token and refresh it before entering the page.
2.路由器实例:大多数前端框架为我们提供了一个路由器/路由实例,以在我们的应用程序中导航用户。 这些路由实例通常基于各种导航事件/阶段来支持钩子,例如在页面加载之前或在页面进入之前等等。 路由器实例将是挂钩我们的刷新令牌解决方案的好地方,因为当用户在页面之间转换时或从最小化的浏览器选项卡重新添加应用程序时,我们可以快速检查当前令牌的有效性并刷新它在进入页面之前。
WHAT DATA DO WE NEED TO IMPLEMENT A REFRESH-TOKEN SOLUTION?
我们需要什么数据来实施刷新令牌解决方案?
Now that we have been able to understand how a refresh token should work we need to understand the data we would need to implement this.
既然我们已经能够了解刷新令牌应如何工作,我们需要了解实现此目标所需的数据。
- Expiry: To implement a refresh-token solution, especially for our router instance we need an expiry value, which would have the sole purpose of telling us when the token is expired. This value would be set with a post-dated timestamp of say 5mins before the back-end service would normally flag the token invalid. This is so that across refreshes and reloads we can know that our token has expired and this value will be updated whenever the token is updated. 到期:要实现刷新令牌解决方案,尤其是对于我们的路由器实例,我们需要一个到期值,其唯一目的是告诉我们令牌何时到期。 在后端服务通常将令牌标记为无效之前,将使用例如5分钟的后时间戳设置此值。 这样一来,在刷新和重新加载过程中,我们可以知道令牌已经过期,并且只要令牌更新,该值就会更新。
- Token: This is a no brainer as in order to refresh a token we would need the old token. To that effect, we need to store our bearer token anytime an authentication is complete as when we have the current token stored the moment it expires we use it to request for a new one and as such it should also be updated with the most recent token. 令牌:这是没有道理的,因为要刷新令牌,我们需要旧令牌。 为此,我们需要在身份验证完成后随时存储承载令牌,因为当当前令牌过期时,当我们存储了当前令牌时,我们会使用它来请求新的令牌,因此也应该使用最新的令牌进行更新。
WHERE/HOW WOULD WE STORE/READ OUR REFRESH-TOKEN RELATED DATA?
我们将在哪里/如何存储/读取我们的刷新令牌相关数据?
Completing the previous step the next question is where these refresh-token data would be stored? Spoiler Alert, Localstorage. We chose to use localstorage because this data has to survive across page refreshes and localstorage is one of the easiest Web Apis to use when persisting data across browser sessions.
完成上一个步骤后,下一个问题是这些刷新令牌数据将存储在哪里? 扰流板警报,本地存储。 我们之所以选择使用本地存储,是因为该数据必须在页面刷新后仍然存在,并且本地存储是跨浏览器会话持久存储数据时最容易使用的Web Apis之一。
- // helper encryption library https://www.npmjs.com/package/crypto-js
- const CryptoJS = require("crypto-js");
-
-
- // encryption secret key, ideally should be an environment variable
- const secret = "secret";
-
-
- // expiry duration in milliseconds, ensure you calculate the expiry in milliseconds
- const expiryDuration = 1680000;
-
-
- // encrypt data
- const encrypt = (data) => {
- if (data != null) {
- return CryptoJS.AES.encrypt(
- JSON.stringify(data),
- secret
- ).toString();
- }
- return null;
- };
-
-
- // decrypt encrypted data
- const decrypt = ciphertext => {
- try {
- if (
- ciphertext != null &&
- ciphertext !== "null"
- ) {
- let bytes = CryptoJS.AES.decrypt(ciphertext.toString(), secret);
- let decrypted = bytes.toString(CryptoJS.enc.Utf8);
- return JSON.parse(decrypted);
- }
- return null;
- } catch (e) {
- return null;
- }
- };
-
-
- // store in localStorage
- const store = (key,value) => {
- return localStorage.setItem(key,value);
- };
-
-
- // read from localstorage
- const read = (key) => {
- return localStorage.getItem(key);
- };
-
-
- // get new expiry
- const getExpiry = () => {
- return (new Date().getTime() + expiryDuration);
- };
-
-
- // check if expired
- const isExpired = (expiry) => {
- return (new Date().getTime() > parseInt(expiry, 10));
- };
-
-
- // Encrypt and store with time expiry functionality
- const storeExpiry = (key, value, expiry = false) => {
- const encryptedData = encrypt(value);
- if (expiry === true) {
- const encryptedExpiry = encrypt(getExpiry());
- store(`${key}.e`,encryptedExpiry);
- }
- return store(key,encryptedData);
- };
-
-
- // decrypt and read with time expiry functionality
- const readExpiry = key => {
- const expiryData = decrypt(read(`${key}.e`));
- const data = decrypt(read(key));
- if (data != null) {
- if (data && isExpired(expiryData)) {
- return { response: data, expired: true };
- }
- if (data && !isExpired(expiryData)) {
- return { response: data, expired: false };
- }
- }
- return {response: null, expired: true};
- };
-
-
- // reset localstorage
- const clear = () => {
- localStorage.clear();
- return null;
- };
-
-
- module.exports = { encrypt, decrypt, clear, storeExpiry, readExpiry, read, store };
Above is the helper file we would use to help with storing and reading from the localstorage with encryption built-in.
上面是帮助程序文件,我们将使用其内置的加密功能来帮助存储和读取本地存储。
HOW DO WE IMPLEMENT THE REFRESH TOKEN FUNCTIONALITY WITH AXIOS?
我们如何使用AXIOS来实现刷新令牌功能?
Now, to get our hands dirty. With our localstorage helper methods, we need to set up the integration with axios. To integrate with axios we are going to be making use of axios interceptors (https://github.com/axios/axios#interceptors).
现在,要弄脏我们的手。 使用我们的本地存储帮助器方法,我们需要设置与axios的集成。 为了与axios集成,我们将使用axios拦截器( https://github.com/axios/axios#interceptors )。
- const { decrypt, storeExpiry, read } = require("./localstorage-helper.js");
- window.axios = require("axios");
-
-
- // set initial default from localstorage
- window.axios.defaults.headers.common["Authorization"] = "Bearer " + decrypt(read("auth_token"));
-
-
- // helper method to refresh token
- async refreshToken() {
- // place request to backend service to refresh token
- const response = await axios.put("/api/auth/refresh-token");
- // update stored instance
- storeExpiry("auth_token", response.data.token, true);
- // update axios instance with new token
- window.axios.defaults.headers.common["Authorization"] = `Bearer ${response.data.token}`;
- },
-
-
- // Add a request interceptor
- window.axios.interceptors.request.use(
- function (config) {
- console.log(`${config.method.toUpperCase()} Request made to ${config.url} with data:`, config.data);
- return config;
- },
- function (err) {
- console.log(err);
- return err;
- });
-
-
- // Add a response interceptor
- window.axios.interceptors.response.use(
- function (response) {
- const { status, data, config } = response;
- console.log(`Response from ${config.url}:`, {
- code: status,
- ...data,
- });
- return response;
- },
- async function (error) {
- if (error.response) {
- const { status, data } = error.response;
-
- switch (status) {
- case 401:
- // check if 401 error was token
- if (data.message === "An unauthenticated request was made, Please try again") {
- // token has expired;
- try {
- // attempting to refresh token;
- await refreshToken();
- // token refreshed, reattempting request;
- const config = error.config;
- // configure new request in a new instance;
- return await window.axios({method: config.method, url: config.url, data: config.data});
- } catch (e) {
- // console.log(e);
- return window.location.href = "/error-page";
- }
- } else {
- return window.location.href = "/error-page";
- }
- default:
- return Promise.reject(error);
- }
- } else if (error.request) {
- // The request was made but no response was received
- // `error.request` is an instance of XMLHttpRequest in the browser and an instance of
- // http.ClientRequest in node.js
- return Promise.reject(error);
- } else {
- // Something happened in setting up the request that triggered an Error
- return Promise.reject(error);
- }
- }
- );
From the file above you can see that the flow is to check the error message from the API and if it's our expected error message for an invalid token, we refresh the token and as soon as that is complete we retry the initial request with the updated axios instance, Easy.
从上面的文件中,您可以看到流程是检查来自API的错误消息,如果这是我们预期的错误令牌无效消息,我们将刷新令牌,并在完成后立即尝试使用更新后的初始请求axios实例,简单。
HOW DO WE IMPLEMENT THE REFRESH TOKEN FUNCTIONALITY WITH ROUTER?
我们如何使用路由器实现刷新令牌功能?
Now to integrate with our routing library. My routing library of choice for this example is the Vue router because why not vue, lol. This should be extensible and simple enough to make meaning in any other routing library.
现在与我们的路由库集成。 在此示例中,我选择的路由库是Vue路由器,因为为什么不使用vue,哈哈。 这应该是可扩展的并且足够简单,以使其在任何其他路由库中都有意义。
- const VueRouter = require("vue-router");
- const axios = require("axios");
-
-
- import {
- readExpiry,
- storeExpiry,
- decrypt
- } from "./localstorage-helper";
-
-
- let routes = [
- // routes definition here
- ];
-
-
- const router = new VueRouter({
- routes,
- linkActiveClass: "active",
- mode: "history",
- scrollBehavior() {
- return { x: 0, y: 0 };
- }
- });
-
-
- // https://router.vuejs.org/guide/advanced/navigation-guards.html#global-before-guards
- router.beforeEach(async (to, from, next) => {
- const protectedRouteNames = ['Home','Settings'];
- const unprotectedRouteNames = ['Login', 'Register', 'Landing'];
- // For Protected Routes
- if (protectedRouteNames.includes(to.name)) {
- try {
- const token = readExpiry("auth_token");
-
-
- // if token is available and not expired
- if (token.response != null && token.expired === false) {
- return next();
- }
-
-
- // if token is available and expired
- if (token.response != null && token.expired === true) {
- // attempt to refresh token
- const response = await axios.put("/api/auth/refresh-token");
- storeExpiry("auth_token", response.data.token, true);
- axios.defaults.headers.common[
- "Authorization"
- ] = `Bearer ${response.data.token}`;
- return next();
- }
- // if token is unavailable
- if (token == null || token.response == null) {
- return next({ name: "Login" });
- }
- } catch (e) {
- return next({ name: "Login" });
- }
- }
-
-
- // Unprotected Routes
- if (unprotectedRouteNames.includes(to.name)) {
- // allow through
- return next();
- }
- // Default Route Action
- return next();
- });
-
-
- module.exports = router;
The flow is before any route action is completed check if it is a protected route that would need a valid token. if the token is valid? allow through, if the token is not valid? attempt to refresh token then allow through if successful. The rest are all fallbacks for errors or handles for unprotected routes.
该流程在任何路由动作完成之前进行检查,检查它是否是需要有效令牌的受保护路由。 令牌是否有效? 允许通过,如果令牌无效? 尝试刷新令牌,如果成功,则允许通过。 其余的都是错误的后备或未受保护的路由的句柄。
The concept of implementing a refresh-token functionality is easy and I hope this tutorial has shown you how to go by it in your daily development should such a requirement come up. Happy Coding.
实现刷新令牌功能的概念很简单,我希望本教程向您展示了在出现这种要求时如何在日常开发中使用它。 编码愉快。
翻译自: https://medium.com/swlh/how-to-implement-refresh-token-functionality-front-end-eff58ce52564