刷新访问令牌并调用转发(操作)后不重新执行查询

Posted

技术标签:

【中文标题】刷新访问令牌并调用转发(操作)后不重新执行查询【英文标题】:Query not re-executed after refreshing access token and calling foward(operation) 【发布时间】:2021-01-06 11:35:55 【问题描述】:

我觉得我的知识可能只是一个根本性的差距,因为我是 Apollo Client 的新手。但我已经仔细阅读了 Stack Overflow、GitHub 问题和 Google,以找到针对我遇到的问题的明显解决方案,但没有找到任何解决方案。

基本上我有以下 Apollo 客户端设置(简化):

const auth = new Auth()
const authMiddleware = new ApolloLink((operation, forward) => 
  const authToken = auth.getToken().access_token

  console.log(authToken)

  operation.setContext(( headers =  ) => (
    headers: 
      ...headers,
      authorization: authToken ? `Bearer $authToken` : ''
    
  ))

  return forward(operation)
)
const cache = new InMemoryCache()
const errorLink = onError(( forward, graphQLErrors, networkError, operation ) => 
  if (graphQLErrors) 
    graphQLErrors.forEach(( extensions, locations, message, path ) => 
      if (extensions.code === 'access-denied') 
        auth.refresh()
          .then(() => 
            console.log(`new access token: $auth.getToken().access_token`)
            return forward(operation)
          ).catch((error) => 
            handleLogout(error)
          )
      
    )
  
)
const handleLogout = (reason) => 
  auth.logout()

const httpLink = new HttpLink( uri: '' )

const client = new ApolloClient(
  cache: cache,
  link: ApolloLink.from([
    errorLink,
    authMiddleware,
    httpLink
  ])
)

我有一个简单的查询:

client.query(
  query: Queries.MyQuery
).then((response) => 
  console.log(response)
, (error) => 
  console.log(error)
)

如果在第一次运行时存在有效的 OAuth 访问令牌,则客户端成功执行查询。但是,如果我使 OAuth 服务器上的访问令牌过期,然后尝试执行查询,则不会成功完成。

调试的时候,我可以看到发生了什么:

    authMiddleware 将旧的访问令牌正确添加到请求标头中。 请求失败,因为令牌不再有效。这是由errorLink 处理的财产。 errorLink 也成功检索到新的访问令牌并返回 forward(operation)。 再次调用authMiddleware,添加新的访问令牌,并返回forward(operation)

这就是事情崩溃的地方。查询永远不会重新执行。如果我手动刷新页面以重新执行查询,它将使用新的访问令牌并成功完成。

通过阅读文档,听起来我设置它的方式应该可以工作,但显然我做错了。

【问题讨论】:

值得记住的是,forEach 循环不能处理异步调用。因此,即使使用正确的刷新/转发(操作)设置,这个循环也需要替换为可以处理异步的常规 for 或 for/ 是的。好点子,@Justin.Mathew! 【参考方案1】:

通过挖掘各种来源,我能够拼凑出正在发生的事情。这主要是因为很多开发者在过去(现在似乎仍然如此)一直在努力解决这个问题,所以有很多过时的解决方案和帖子。

This GitHub issue 是最有用的信息来源,尽管它附加到现在已弃用的存储库。 This Stack Overflow answer 也很有帮助。

我花了一些时间使用实用方法将 Promise 转换为 Observable,但如果您使用 fromPromise,则不再需要这样做。

这是我最终得到的适用于 Apollo Client 3.2.0 的解决方案:

const authLink = new ApolloLink((operation, forward) => 
  const authToken = auth.getToken().access_token

  console.info(`access token: $authToken`)
  operation.setContext(( headers ) => (
    headers: 
      ...headers,
      authorization: authToken ? `Bearer $authToken` : ''
    
  ))

  return forward(operation)
)
const errorLink = onError(( graphQLErrors, networkError, operation, forward ) => 
  if (graphQLErrors) 
    const firstGraphQLError = graphQLErrors[0]

    if (firstGraphQLError.extensions.code === 'access-denied') 
      let innerForward

      if (!isRefreshing) 
        isRefreshing = true
        innerForward = fromPromise(
          auth.refresh()
            .then(() => 
              const authToken = auth.getToken().access_token
              console.info(`access token refreshed: $authToken`)
              resolvePendingRequests()
              return authToken
            )
            .catch(() => 
              pendingRequests = []
              // Log the user out here.
              return false
            )
            .finally(() => 
              isRefreshing = false
            )
        ).filter(value => Boolean(value))
       else 
        innerForward = fromPromise(
          new Promise(resolve => 
            pendingRequests.push(() => resolve())
          )
        )
      

      return innerForward.flatMap(() => 
        return forward(operation)
      )
     else 
      console.log(`[GraphQL error]: Message: $firstGraphQLError.message, Location: $firstGraphQLError.locations, Path: $firstGraphQLError.path`)
    
  

  if (networkError) 
    console.log(`[Network error]: $networkError`)
  
)

const client = new ApolloClient(
  cache: new InMemoryCache(),
  link: from([
    errorLink,
    authLink,
    new HttpLink( uri: '' )
  ])
)

此解决方案还处理多个并发请求,将它们排队并在访问令牌刷新后请求它们。

【讨论】:

以上是关于刷新访问令牌并调用转发(操作)后不重新执行查询的主要内容,如果未能解决你的问题,请参考以下文章

OAuth - 在“社交登录”中使用刷新令牌

如何在资源请求上重新生成刷新令牌和访问令牌?

无法刷新访问令牌。请重新认证

OAuth 客户端凭据重新颁发访问令牌与刷新令牌

JWT 令牌刷新后返回重新调用

如何刷新 jwt 并重新发送失败的 http 请求?