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

use*_*462 15 apollo graphql react-native apollo-client

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

Apollo-Link-Error 的 Apollo Docs 提供了一个很好的起点来捕获来自 ApolloClient 的错误:

onError(({ graphQLErrors, networkError, operation, forward }) => {
  if (graphQLErrors) {
    for (let err of graphQLErrors) {
      switch (err.extensions.code) {
        case 'UNAUTHENTICATED':
          // error code is set to UNAUTHENTICATED
          // when AuthenticationError thrown in resolver

          // modify the operation context with a new token
          const oldHeaders = operation.getContext().headers;
          operation.setContext({
            headers: {
              ...oldHeaders,
              authorization: getNewToken(),
            },
          });
          // retry the request, returning the new observable
          return forward(operation);
      }
    }
  }
})
Run Code Online (Sandbox Code Playgroud)

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

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

Léo*_*gli 18

Apollo Error Link 文档中给出的示例是一个很好的起点,但假设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);
Run Code Online (Sandbox Code Playgroud)

您可以停留在上述运行正常的实现上,直到两个或多个请求同时失败。因此,要在令牌到期时处理并发请求失败,请查看这篇文章

  • @MustKillBill 此工作流程用于基于标头的身份验证,其中客户端可以访问、设置或存储 jwt。在基于 cookie 的身份验证中,客户端无法使用 JavaScript 访问 cookie,因为它们通常标记为 HTTPOnly。因此,服务器需要使用 Set-Cookie HTTP 标头来发送 cookie,该标头指示 Web 浏览器存储 cookie 并在将来向服务器发出请求时将其发送回。 (5认同)

BIl*_*igt 13

更新 - 2022 年 1 月, 您可以从以下位置查看基本的 React JWT 身份验证设置:https://github.com/bilguun-zorigt/React-GraphQL-JWT-Authentication-Example

我还在存储库的自述文件部分添加了在前端和后端设置身份验证时要考虑的安全点。(XSS攻击、csrf攻击等...)

原始答案 - 2021 年 12 月

我的解决方案:

  • 适用于并发请求(通过对所有请求使用单一承诺)
  • 不等待错误发生
  • 使用第二个客户端进行刷新突变
import { setContext } from '@apollo/client/link/context';

async function getRefreshedAccessTokenPromise() {
  try {
    const { data } = await apolloClientAuth.mutate({ mutation: REFRESH })
    // maybe dispatch result to redux or something
    return data.refreshToken.token
  } catch (error) {
    // logout, show alert or something
    return error
  }
}

let pendingAccessTokenPromise = null

export function getAccessTokenPromise() {
  const authTokenState = reduxStoreMain.getState().authToken
  const currentNumericDate = Math.round(Date.now() / 1000)

  if (authTokenState && authTokenState.token && authTokenState.payload &&
    currentNumericDate + 1 * 60 <= authTokenState.payload.exp) {
    //if (currentNumericDate + 3 * 60 >= authTokenState.payload.exp) getRefreshedAccessTokenPromise()
    return new Promise(resolve => resolve(authTokenState.token))
  }

  if (!pendingAccessTokenPromise) pendingAccessTokenPromise = getRefreshedAccessTokenPromise().finally(() => pendingAccessTokenPromise = null)

  return pendingAccessTokenPromise
}

export const linkTokenHeader = setContext(async (_, { headers }) => {
  const accessToken = await getAccessTokenPromise()
  return {
    headers: {
      ...headers,
      Authorization: accessToken ? `JWT ${accessToken}` : '',
    }
  }
})


export const apolloClientMain = new ApolloClient({
  link: ApolloLink.from([
    linkError,
    linkTokenHeader,
    linkMain
  ]),
  cache: inMemoryCache
});
Run Code Online (Sandbox Code Playgroud)