当多个请求同时发出时如何处理刷新令牌?

33

我正在使用reactjs、mbox和axios,并遇到了一个问题。我有一个API可以提供访问令牌和刷新令牌。访问令牌每20分钟就会失效,当发生这种情况时,服务器会发送401,我的代码将自动发出刷新令牌以获取新的访问令牌。

一旦获得新的访问令牌,同样被拒绝的请求将再次发送。现在我的代码很棒,直到我抛出多个拒绝请求,它们几乎可以同时触发。

所以第一个请求发出去了,发送了401并获得了新的刷新令牌,那么所有其他请求都将尝试做同样的事情,但其他请求现在会失败,因为刷新令牌将被使用,并且新的令牌将被分配给第一个请求。

这将触发我的代码将用户重定向到登录页面。

因此,实际上我只能一次处理1个请求。

export const axiosInstance = axios.create({
    baseURL: getBaseUrl(),
    timeout: 5000,
    contentType: "application/json",
    Authorization: getAuthToken()
  });

  export function updateAuthInstant() {
    axiosInstance.defaults.headers.common["Authorization"] = getAuthToken();
  }


function getAuthToken() {
    if (localStorage.getItem("authentication")) {
      const auth = JSON.parse(localStorage.getItem("authentication"));
      return `Bearer ${auth.accessToken}`;
    }
  }

axiosInstance.interceptors.response.use(
  function(response) {
    return response;
  },
  function(error) {
    const originalRequest = error.config;
    if (error.code != "ECONNABORTED" && error.response.status === 401) {
      if (!originalRequest._retry) {
        originalRequest._retry = true;
        return axiosInstance
          .post("/tokens/auth", {
            refreshToken: getRefreshToken(),
            grantType: "refresh_token",
            clientId : "myclient"
          })
          .then(response => {

            uiStores.authenticaionUiStore.setAuthentication(JSON.stringify(response.data))
            updateAuthInstant();
            return axiosInstance(originalRequest);
          });
      } else {
        uiStores.authenticaionUiStore.logout();
        browserHistory.push({ pathname: '/login',});
      }

    }
    return Promise.reject(error);
  }
);

编辑

我遇到了一个问题,需要检查重置身份验证的代码在用户复制直接网址时不起作用。

app.js

  <React.Fragment>
       <Switch>
          <Route path="/members" component={MemberAreaComponent} />
        </Switch>
  </React.Fragment >

在memberAreaComponent中

      <Route path="/members/home" component={MembersHomeComponent} />

当我输入http://www.mywebsite/members/home

MembersHomeComponent - componentDidMount runs first
MemberAreaComponent - componentDidMount runs second
AppCoontainer = componentDidMount runs last.

1
很好的问题,不确定我有多喜欢提出的解决方案,你最终做了什么? - OverMars
1个回答

6
你好,我是一名有用的助手,可以将文本翻译成中文。
英译中:

嗨,我已经在react/redux应用程序中实现了相同的场景。但这可以帮助你实现目标。您不需要在每个API调用中检查401。只需在第一个验证API请求中实现它即可。您可以使用setTimeOut在身份验证令牌到期之前的某个时间发送刷新令牌API请求。因此,locatStorage将得到更新,所有axios请求将永远不会过期。

这是我的解决方案:

在我的Constants.js中,我像这样在localStorage中维护USER TOKEN:

 export const USER_TOKEN = {
   set: ({ token, refreshToken }) => {
      localStorage.setItem('access_token', token);
      localStorage.setItem('refresh_token', refreshToken);
   },
   remove: () => {
      localStorage.removeItem('access_token');
      localStorage.removeItem('refresh_token');
 },
   get: () => ({
     agent: 'agent',
     token: localStorage.getItem('access_token'),
     refreshToken: localStorage.getItem('refresh_token'),
  }),
   get notEmpty() {
      return this.get().token !== null;
  },
};

export const DEFAULT_HEADER = {
     get: () => ({
      'Content-type': 'application/json;charset=UTF-8',
       agent: `${USER_TOKEN.get().agent}`,
       access_token: `${USER_TOKEN.get().token}`,
 }),
};

在页面加载时,用户验证 API 请求如下:
dispatch(actions.validateUser(userPayload)) // First time authentication with user credentials and it return access token, refresh token and expiry time
  .then(userData => {
    const { expires_in, access_token, refresh_token } = userData
    USER_TOKEN.set({          // setting tokens in localStorage to accessible to all API calls
      token: access_token,
      refreshToken: refresh_token,
    });
    const timeout = expires_in * 1000 - 60 * 1000; // you can configure as you want but here it is 1 min before token will get expired
    this.expiryTimer = setTimeout(() => {  // this would reset localStorage before token expiry timr
      this.onRefreshToken();
    }, timeout);
  }).catch(error => {
    console.log("ERROR", error)
  });

onRefreshToken = () => {
   const { dispatch } = this.props;
   const refresh_token = USER_TOKEN.get().refreshToken;
   dispatch(actions.refreshToken({ refresh_token })).then(userData => {
      const { access_token, refresh_token } = userData
      USER_TOKEN.set({
         token: access_token,
          refreshToken: refresh_token,
    });
  });
};

随时提出任何问题,另一种方法是使用axios的abort controller来取消待处理的promise。我也很乐意帮助你!编辑-你可以在所有API请求中维护axios token source以随时中止它们。在所有API中维护axios token source。一旦你得到第一个promise解决,就可以取消所有其他挂起的API请求。在第一个promise解决后,你可以调用onAbort方法。请参考以下内容:
//in your component
class MyComponent extends Component{
isTokenSource = axios.CancelToken.source(); // a signal you can point to any API

componentDidMount{
   // for example if you're sending multiple api call here
        this.props.dispatch(actions.myRequest(payload, this.isTokenSource.token))
        .then(() => {
            // all good
        })
        .catch(error => {
            if (axios.isCancel(error)) {
                console.warn('Error', error);
            }
        });
}

onAbortStuff = () => {  // cancel request interceptor
    console.log("Aborting Request");
    this.isTokenSource.cancel('API was cancelled'); // This will abort all the pending promises if you send the same token in multiple requests, 
}

render(){
//
}

在 axios 请求中,你可以这样发送 token:

export const myRequest= (id, cancelToken) => {
    const URL = `foo`;
    return axios(URL, {
      method: 'GET',
      headers: DEFAULT_HEADER.get(),
      cancelToken: cancelToken
    })
.then(response => {
  // handle success
  return response.data;
  })
.catch(error => {
  throw error;
   });
  };

供参考,您可以阅读这篇文章,它对于了解如何取消订阅非常有帮助。链接为:https://medium.freecodecamp.org/how-to-work-with-react-the-right-way-to-avoid-some-common-pitfalls-fc9eb5e34d9e 您可以按照以下方式构建您的路由: index.js
<Provider store={store}>
  <BrowserRouter>
    <App />
  </BrowserRouter>
</Provider>

App.js:

class App extends Component {


state = {
    isAuthenticated: false,
  };

  componentDidMount() {
   //authentication API and later you can setState isAuthenticate
   }
    render() {
    const { isAuthenticated } = this.state;
    return isAuthenticated ? <Routes /> : <Loading />;
  }

如果您仍然发现任何问题,我非常乐意帮助您解决。


1
嗯,有趣。我以为你会使用间隔计时器,但也许我没看到,你是如何保持计时器运行的?我猜因为访问令牌被接受只要它们过期了,即使在发送旧令牌时创建了新令牌,它仍然会被允许通过?如果刷新令牌无效,你的代码如何处理,你应该强制将他们从你的网站中移除?还是检查401错误码? - chobo2
关于中止请求的第二个问题,您可以查看我上面编辑过的答案。 - Sakhi Mansoor
1
我也选择了选项1。当他们关闭浏览器后,再通过复制链接回来时,我遇到了问题。因此,我将我的代码放在app.js中检查他们是否具有身份验证。当他们要访问的组件在app.js之前运行时,我会重定向到我的登录页面,因为ajax请求在401状态下返回。我需要一些在其他事件之前触发的事件。 - chobo2
1
嗯,我正在查看我的代码。我有一个名为MembersArea的路由到我的管理部分,但是当我直接在URL中输入管理区域时,管理员组件会在memberarea组件之前运行。 - chobo2
@chobo2 在所有路由上应该有一个组件,如果您使用任何URL加载应用程序,则首先呈现该组件。 - Sakhi Mansoor
显示剩余8条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接