如何并行处理多个请求/ API调用的客户端JWT刷新令牌?

时间:2020-03-30 23:48:53

标签: javascript security authentication jwt authorization

我遇到一个问题,我在任何地方都找不到安全第一且可维护的答案。

想象一下一个仪表板同时执行多个查询,您如何以一种干净,标准的方式处理 refresh_tokens

堆栈是(即使此处无所谓):

后端-具有JWT令牌认证的Laravel 前端-带有用于API调用的axios的Vue JS

端点:

  • / auth / login(公开)
  • / auth / refresh-token(需要身份验证)
  • / statistics(需要身份验证)
  • / other-statistics(需要身份验证)
  • / event-more-statistics(需要身份验证)
  • / final-statistics(需要身份验证) ...

JWT刷新工作流程

  • 用户导航到客户端上的 mywebsite.com/login
  • “登录”页面对服务器 /* Tablet and up */ @media screen and (min-width: 768px) { .carousel-inner .active, .carousel-inner .active+.carousel-item { display: block; } .carousel-inner .carousel-item.active:not(.carousel-item-right):not(.carousel-item-left), .carousel-inner .carousel-item.active:not(.carousel-item-right):not(.carousel-item-left)+.carousel-item { -webkit-transition: none; transition: none; } .carousel-inner .carousel-item-next, .carousel-inner .carousel-item-prev { position: relative; -webkit-transform: translate3d(0, 0, 0); transform: translate3d(0, 0, 0); } .carousel-inner .active.carousel-item+.carousel-item+.carousel-item+.carousel-item { position: absolute; top: 0; right: -50%; z-index: -1; display: block; visibility: visible; } /* left or forward direction */ .active.carousel-item-left+.carousel-item-next.carousel-item-left, .carousel-item-next.carousel-item-left+.carousel-item { position: relative; -webkit-transform: translate3d(-100%, 0, 0); transform: translate3d(-100%, 0, 0); visibility: visible; } /* farthest right hidden item must be abso position for animations */ .carousel-inner .carousel-item-prev.carousel-item-right { position: absolute; top: 0; left: 0; z-index: -1; display: block; visibility: visible; } /* right or prev direction */ .active.carousel-item-right+.carousel-item-prev.carousel-item-right, .carousel-item-prev.carousel-item-right+.carousel-item { position: relative; -webkit-transform: translate3d(100%, 0, 0); transform: translate3d(100%, 0, 0); visibility: visible; display: block; visibility: visible; } } /* Desktop and up */ @media screen and (min-width: 992px) { .carousel-inner .active, .carousel-inner .active+.carousel-item, .carousel-inner .active+.carousel-item+.carousel-item { display: block; } .carousel-inner .carousel-item.active:not(.carousel-item-right):not(.carousel-item-left), .carousel-inner .carousel-item.active:not(.carousel-item-right):not(.carousel-item-left)+.carousel-item, .carousel-inner .carousel-item.active:not(.carousel-item-right):not(.carousel-item-left)+.carousel-item+.carousel-item { -webkit-transition: none; transition: none; } .carousel-inner .carousel-item-next, .carousel-inner .carousel-item-prev { position: relative; -webkit-transform: translate3d(0, 0, 0); transform: translate3d(0, 0, 0); } .carousel-inner .active.carousel-item+.carousel-item+.carousel-item+.carousel-item { position: absolute; top: 0; right: -33.3333%; z-index: -1; display: block; visibility: visible; } /* left or forward direction */ .active.carousel-item-left+.carousel-item-next.carousel-item-left, .carousel-item-next.carousel-item-left+.carousel-item, .carousel-item-next.carousel-item-left+.carousel-item+.carousel-item, .carousel-item-next.carousel-item-left+.carousel-item+.carousel-item+.carousel-item { position: relative; -webkit-transform: translate3d(-100%, 0, 0); transform: translate3d(-100%, 0, 0); visibility: visible; } /* farthest right hidden item must be abso position for animations */ .carousel-inner .carousel-item-prev.carousel-item-right { position: absolute; top: 0; left: 0; z-index: -1; display: block; visibility: visible; } /* right or prev direction */ .active.carousel-item-right+.carousel-item-prev.carousel-item-right, .carousel-item-prev.carousel-item-right+.carousel-item, .carousel-item-prev.carousel-item-right+.carousel-item+.carousel-item, .carousel-item-prev.carousel-item-right+.carousel-item+.carousel-item+.carousel-item { position: relative; -webkit-transform: translate3d(100%, 0, 0); transform: translate3d(100%, 0, 0); visibility: visible; display: block; visibility: visible; } } 进行API调用
  • 服务器响应 access_token (生命周期1分钟)和 refresh_token (生命周期1个月左右)
  • 用户导航到 mywebsite.com/dashboard
  • 用户单击某些内容,“仪表板”页面与上述4个最后一个端点并行进行4个API调用
axios.get('/auth/login').then(res => setTokenAndUser(res))
  • 第一次通话结束,服务器使旧令牌失效,并以新的 access_token refresh_token
  • 响应
  • 第二次呼叫被服务器阻止,因为它正在传输过期的令牌
  • 第三个调用被服务器阻止,因为它正在传输过期的令牌
  • 第四个呼叫被服务器阻止,因为它正在传输过期的令牌
  • 客户端/用户界面未正确更新

这是在SPA和SaaS应用程序中非常常见的情况。拥有多个异步API调用并非偶然。

我在这里有什么选择?

  • 不使令牌无效:
    • 但是随后出现了安全漏洞,使用JWT令牌变得毫无用处
  • 跟踪每个失败的API调用,并在刷新令牌更改时重播它们
    • 这很难维护,并且会在用户界面上为用户创建无法预测的行为
    • 如果用户在呼叫重播期间进行交互,则会使呼叫处理程序混乱
    • 每个axios调用都有一个promise,期望得到良好的处理,我们也需要存储和延迟每个promise,以便正确处理用户界面
    • 每次新的重播也会每次都重新创建新的令牌

我目前的想法是使用以下工作流程使 access_token 持续3天,使 refresh_token 持续一个月:

  • 前端启动时,我们在客户端检查access_token的有效性
    • 如果 refresh_token 已过期,请清除客户端中的令牌
    • 其他什么都不做
    • 如果 access_token 过期时间超过12小时,请发送所有以后的请求
    • 其他使用刷新令牌来获取新令牌

这使 refresh_token 在网络上的传播减少,并且使并行失败成为不可能,因为我们仅在前端加载时才更改令牌,因此,令牌在失败之前至少可以生存12h。

尽管此解决方案有效,但我正在寻找更安全/标准的方式,有任何线索吗?

1 个答案:

答案 0 :(得分:2)

这是我在应用程序中遇到的情况以及解决方法:

应用程序设置

  • Nuxt应用程序
  • 使用axios进行API调用
  • 使用Vuex进行状态管理
  • 使用每15分钟失效一次的JWT令牌,因此无论何时发生这种情况,都应该有一个API调用来刷新令牌并重复失败的请求

令牌

我将令牌数据保存在会话存储中,并每次使用刷新令牌API响应进行更新

问题

我在一个页面中有三个get请求,并且我希望这种行为是当令牌到期时 ONLY (仅),其中一个可以调用刷新令牌API,而其他则必须等待响应,当刷新令牌承诺已解决,他们三个都应使用更新的令牌数据重复失败的请求

使用axios拦截器和vuex的解决方案

这是vuex设置:

// here is the state to check if there is a refresh token request proccessing or not  
export const state = () => ({
  isRefreshing: false,
});

// mutation to update the state
export const mutations = {
  SET_IS_REFRESHING(state, isRefreshing) {
    state.isRefreshing = isRefreshing;
  },
};

// action to call the mutation with a false or true payload
export const actions = {
  setIsRefreshing({ commit }, isRefreshing) {
    commit('SET_IS_REFRESHING', isRefreshing);
  },
};

这是axios设置:

import { url } from '@/utils/generals';

// adding axios instance as a plugin to nuxt app (nothing to concern about!)
export default function ({ $axios, store, redirect }, inject) {

  // creating axios instance
  const api = $axios.create({
    baseURL: url,
  });

  // setting the authorization header from the data that is saved in session storage with axios request interceptor
  api.onRequest((req) => {
    if (sessionStorage.getItem('user'))
      req.headers.authorization = `bearer ${
        JSON.parse(sessionStorage.getItem('user')).accessToken
      }`;
  });

  // using axios response interceptor to handle the 401 error
  api.onResponseError((err) => {
    // function that redirects the user to the login page if the refresh token request fails
    const redirectToLogin = function () {
      // some code here
    };

    if (err.response.status === 401) {
      // failed API call config
      const config = err.config;
      
      // checks the store state, if there isn't any refresh token proccessing attempts to get new token and retry the failed request
      if (!store.state.refreshToken.isRefreshing) {
        return new Promise((resolve, reject) => {
          // updates the state in store so other failed API with 401 error doesnt get to call the refresh token request
          store.dispatch('refreshToken/setIsRefreshing', true);
          let refreshToken = JSON.parse(sessionStorage.getItem('user'))
            .refreshToken;

          // refresh token request
          api
            .post('token/refreshToken', {
              refreshToken,
            })
            .then((res) => {
              if (res.data.success) {
                // update the session storage with new token data
                sessionStorage.setItem(
                  'user',
                  JSON.stringify(res.data.customResult)
                );
                // retry the failed request 
                resolve(api(config));
              } else {
                // rediredt the user to login if refresh token fails
                redirectToLogin();
              }
            })
            .catch(() => {
                // rediredt the user to login if refresh token fails
              redirectToLogin();
            })
            .finally(() => {
              // updates the store state to indicate the there is no current refresh token request and/or the refresh token request is done and there is updated data in session storage
              store.dispatch('refreshToken/setIsRefreshing', false);
            });
        });
      } else {
        // if there is a current refresh token request, it waits for that to finish and use the updated token data to retry the API call so there will be no Additional refresh token request
        return new Promise((resolve, reject) => {
          // in a 100ms time interval checks the store state
          const intervalId = setInterval(() => {
            // if the state indicates that there is no refresh token request anymore, it clears the time interval and retries the failed API call with updated token data
            if (!store.state.refreshToken.isRefreshing) {
              clearInterval(intervalId);
              resolve(api(config));
            }
          }, 100);
        });
      }
    }
  });

  // injects the axios instance to nuxt context object (nothing to concern about!)
  inject('api', api);
}

这是“网络”标签中显示的情况:

enter image description here

如您所见,这里有三个失败的请求,出现401错误,然后有一个refreshToken请求,之后所有失败的请求都被更新的令牌数据再次调用

相关问题