如何使用Apollo和GraphQL刷新JWT令牌

时间:2020-04-20 16:32:40

标签: react-native graphql apollo apollo-client

因此,我们正在使用Apollo和GraphQL创建一个React-Native应用程序。我正在使用基于JWT的身份验证(当用户同时登录 activeToken refreshToken 时均已创建),并且想要实现一个流程,当服务器注意到令牌已过期时,令牌会自动刷新。

Apollo链接错误的Apollo文档提供了很好的starting point来捕获ApolloClient的错误:

export REPO="wordpress-sites"

但是,我真的很难弄清楚如何实现 getNewToken()。 我的GraphQL端点具有用于创建新令牌的解析器,但是我无法从Apollo-Link-Error调用它吗?

如果令牌是在Apollo客户端将连接到的GraphQL端点中创建的,那么如何刷新令牌?

2 个答案:

答案 0 :(得分:9)

Apollo错误链接文档中给出的example是一个很好的起点,但是假设 getNewToken()操作是同步的。 对于您的情况,您必须点击GraphQL端点才能获取新的访问令牌。这是异步操作,您必须使用apollo-link包中的 fromPromise 实用程序功能将Promise转换为Observable。

import React from "react";
import { AppRegistry } from 'react-native';

import { onError } from "apollo-link-error";
import { fromPromise, ApolloLink } from "apollo-link";
import { ApolloClient } from "apollo-client";

let apolloClient;

const getNewToken = () => {
  return apolloClient.query({ query: GET_TOKEN_QUERY }).then((response) => {
    // extract your accessToken from your response data and return it
    const { accessToken } = response.data;
    return accessToken;
  });
};

const errorLink = onError(
  ({ graphQLErrors, networkError, operation, forward }) => {
    if (graphQLErrors) {
      for (let err of graphQLErrors) {
        switch (err.extensions.code) {
          case "UNAUTHENTICATED":
            return fromPromise(
              getNewToken().catch((error) => {
                // Handle token refresh errors e.g clear stored tokens, redirect to login
                return;
              })
            )
              .filter((value) => Boolean(value))
              .flatMap((accessToken) => {
                const oldHeaders = operation.getContext().headers;
                // modify the operation context with a new token
                operation.setContext({
                  headers: {
                    ...oldHeaders,
                    authorization: `Bearer ${accessToken}`,
                  },
                });

                // retry the request, returning the new observable
                return forward(operation);
              });
        }
      }
    }
  }
);

apolloClient = new ApolloClient({
  link: ApolloLink.from([errorLink, authLink, httpLink]),
});

const App = () => (
  <ApolloProvider client={apolloClient}>
    <MyRootComponent />
  </ApolloProvider>
);

AppRegistry.registerComponent('MyApplication', () => App);

您可以停止上面的实现,直到两个或多个请求同时失败为止。因此,要处理令牌到期时的并发请求失败,请查看此post

答案 1 :(得分:0)

如果您使用的是 JWT,您应该能够检测到您的 JWT 令牌何时即将过期或是否已经过期。

因此,您无需发出总是因 401 未授权而失败的请求。

您可以通过以下方式简化实现:

const REFRESH_TOKEN_LEGROOM = 5 * 60

export function getTokenState(token?: string | null) {
    if (!token) {
        return { valid: false, needRefresh: true }
    }

    const decoded = decode(token)
    if (!decoded) {
        return { valid: false, needRefresh: true }
    } else if (decoded.exp && (timestamp() + REFRESH_TOKEN_LEGROOM) > decoded.exp) {
        return { valid: true, needRefresh: true }
    } else {
        return { valid: true, needRefresh: false }
    }
}


export let apolloClient : ApolloClient<NormalizedCacheObject>

const refreshAuthToken = async () => {
  return apolloClient.mutate({
    mutation: gql```
    query refreshAuthToken {
      refreshAuthToken {
        value
      }```,
  }).then((res) => {
    const newAccessToken = res.data?.refreshAuthToken?.value
    localStorage.setString('accessToken', newAccessToken);
    return newAccessToken
  })
}

const apolloHttpLink = createHttpLink({
  uri: Config.graphqlUrl
})

const apolloAuthLink = setContext(async (request, { headers }) => {
  // set token as refreshToken for refreshing token request
  if (request.operationName === 'refreshAuthToken') {
    let refreshToken = localStorage.getString("refreshToken")
    if (refreshToken) {
      return {
        headers: {
          ...headers,
          authorization: `Bearer ${refreshToken}`,
        }
      }
    } else {
      return { headers }
    }
  }

  let token = localStorage.getString("accessToken")
  const tokenState = getTokenState(token)

  if (token && tokenState.needRefresh) {
    const refreshPromise = refreshAuthToken()

    if (tokenState.valid === false) {
      token = await refreshPromise
    }
  }

  if (token) {
    return {
      headers: {
        ...headers,
        authorization: `Bearer ${token}`,
      }
    }
  } else {
    return { headers }
  }
})

apolloClient = new ApolloClient({
  link: apolloAuthLink.concat(apolloHttpLink),
  cache: new InMemoryCache()
})

这种实现的优点:

  • 如果访问令牌即将到期 (REFRESH_TOKEN_LEGROOM),它将在不停止当前查询的情况下请求刷新令牌。哪些应该对您的用户不可见
  • 如果访问令牌已经过期,它会刷新令牌并等待响应更新它。比等待错误返回要快得多

缺点:

  • 如果您一次发出许多请求,它可能会请求多次刷新。例如,您可以通过等待全局承诺来轻​​松防范它。但是,如果您只想保证一次刷新,则必须实施适当的竞争条件检查。