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

Posted

技术标签:

【中文标题】如何在客户端处理多个请求/API 调用并行的 JWT 刷新令牌?【英文标题】:How to handle JWT refresh token on client-side with multiple requests / API calls in parallel? 【发布时间】:2020-07-11 10:59:30 【问题描述】:

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

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

堆栈是(即使堆栈在这里无​​关紧要):

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

端点:

/auth/login(公共) /auth/refresh-token(需要授权) /statistics(需要授权) /other-statistics(需要授权) /event-more-statistics(需要授权) /final-statistics(需要授权) ...

JWT 刷新工作流程

用户导航到客户端上的 mywebsite.com/login 登录页面对服务器进行 API 调用axios.get('/auth/login').then(res => setTokenAndUser(res)) 服务器响应 access_token(生命周期 1 分钟)和 refresh_token(生命周期 1 个月左右) 用户导航到 mywebsite.com/dashboard 用户点击某物,仪表板页面执行 4 个 API 调用,与上面的最后 4 个端点并行
// ... just some pseudo code
userDidAction() 
  axios.get('/statistics').then(res => handleThis(res.data));
  axios.get('/other-statistics').then(res => handleThat(res.data));
  axios.get('/event-more-statistics').then(res => handleThisAgain(res.data));
  axios.get('/final-statistics').then(res => handleThatAgain(res.data));

// ...
第一次调用结束,服务器使旧令牌无效 + 以新的 access_tokenrefresh_token 响应 第二次调用被服务器阻止,因为它正在传输一个过期的令牌 第三次调用被服务器阻止,因为它正在传输一个过期的令牌 第 4 次调用被服务器阻止,因为它正在传输过期的令牌 客户端/用户界面未正确更新

这是 SPA 和 SaaS 应用程序中非常常见的场景。拥有多个异步 API 调用并不是极端情况。

我有哪些选择?

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

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

前端启动时,我们在客户端检查access_token的有效性 如果 refresh_token 已过期,请从客户端清除令牌 其他什么都不做 如果 access_token 在超过 12 小时后过期,请连同它一起发送所有未来的请求 否则使用刷新令牌获取新令牌

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

尽管这个解决方案有效,但我正在寻找一种更安全/标准的方式,有什么线索吗?

【问题讨论】:

【参考方案1】:

以下是我在应用程序中遇到的情况以及我解决它的方式:

应用设置

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

令牌

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

问题

我在一个页面中有三个获取请求,我希望这种行为在令牌过期时其中一个可以调用刷新令牌 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);

这是网络选项卡中显示的情况:

如您在此处看到的,有 3 个失败的请求并出现 401 错误,然后有一个 refreshToken 请求,之后所有失败的请求都被再次调用并使用更新的令牌数据

【讨论】:

以上是关于如何在客户端处理多个请求/API 调用并行的 JWT 刷新令牌?的主要内容,如果未能解决你的问题,请参考以下文章

如何并行调用多个 API 进行负载测试(使用 Gatling)?

如何并行进行多个 Spring Webclient 调用并等待结果?

当两个 Ajax 请求被并行调用时,我看到多个加载掩码

Weblogic 9中如何实现并行处理

如何在 Django Rest Framework 中处理并行请求?

js调用webservice接口