【问题标题】:How to refresh JWT token using Apollo and GraphQL如何使用 Apollo 和 GraphQL 刷新 JWT 令牌
【发布时间】:2020-08-03 05:59:53
【问题描述】:

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

Apollo-Link-Error 的 Apollo Docs 提供了一个很好的 starting point 来捕获来自 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);
      }
    }
  }
})

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

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

【问题讨论】:

  • onError 链接在请求后运行。我认为您不能简单地转发再试一次。理想情况下,您可以确定您当前的令牌在前端是否仍然有效,例如通过查看 JWT 中的 exp 声明。然后你可以使用这个极好的链接:github.com/newsiberian/apollo-link-token-refresh
  • 您可以使用window.fetch 调用您的GraphQL enpoint。这需要更多的工作,但对于单个查询来说应该没问题。只需 POST 到端点,其 JSON 对象包含 query 和可选的 variablesoperation

标签: react-native graphql apollo apollo-client


【解决方案1】:

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);

您可以停止上述正常工作的实现,直到两个或多个请求同时失败。因此,要在令牌过期时处理并发请求失败,请查看 this post

【讨论】:

  • 您将如何使用新令牌更新 cookie?
  • @MustKillBill 此工作流用于基于标头的身份验证,客户端可以访问、设置或存储 jwt。在基于 cookie 的身份验证中,客户端无法使用 JavaScript 访问 cookie,因为它们通常被标记为 HTTPOnly。因此,由服务器使用 Set-Cookie HTTP 标头发送 cookie,该标头指示 Web 浏览器存储 cookie 并在将来的请求中将其发送回服务器。
【解决方案2】:

更新 - 2022 年 1 月 您可以从以下位置查看基本的 React JWT 身份验证设置:https://github.com/earthguestg/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
});

【讨论】:

  • 您可以分享您用于 apollo 接口的完整代码吗?理想情况下有一个例子,比如登录?我无法确切说明这将如何与我现有的代码一起使用。
  • 感谢分享这个很好的例子。看起来您使用的是django-graphql-jwt.domake.io/index.html,它使用单个令牌进行身份验证,而我使用的是django-graphql-auth.readthedocs.io,它使用单独的刷新令牌。我在你的代码中添加了一个刷新令牌,并试图让它工作。祝我好运:)
  • 不应该这样:currentNumericDate + 1 * 60(currentNumericDate + 1) * 60 吗?
  • @Stathis Ntonas 1 * 60 只是增加了 1 分钟,这意味着如果令牌不会在 1 分钟内过期,则不需要刷新。
  • @earthguestg 那么在这种情况下currentNumericDate + 60 就足够了,不需要1 * 60
【解决方案3】:

如果您使用的是 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),它将请求刷新令牌而不停止当前查询。您的用户应该看不到哪个
  • 如果访问令牌已经过期,它将刷新令牌并等待响应更新它。比等待错误返回要快得多

缺点:

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

【讨论】:

  • 这是在 react native 顺便说一句,但 web 的逻辑是一样的
【解决方案4】:

在互联网上检查了这个主题和其他一些非常好的主题后,我的代码使用了以下解决方案

  ApolloClient,
  NormalizedCacheObject,
  gql,
  createHttpLink,
  InMemoryCache,
} from '@apollo/client';
import { setContext } from '@apollo/client/link/context';
import jwt_decode, { JwtPayload } from 'jwt-decode';

import {
  getStorageData,
  setStorageData,
  STORAGE_CONTANTS,
} from '../utils/local';

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

  const decoded = jwt_decode<JwtPayload>(token);

  if (!decoded) {
    return { valid: false, needRefresh: true };
  }
  if (decoded.exp && Date.now() >= decoded.exp * 1000) {
    return { valid: false, needRefresh: true };
  }
  return { valid: true, needRefresh: false };
}

export let client: ApolloClient<NormalizedCacheObject>;

const refreshAuthToken = async () => {
  const refreshToken = getStorageData(STORAGE_CONTANTS.REFRESHTOKEN);

  const newToken = await client
    .mutate({
      mutation: gql`
        mutation RefreshToken($refreshAccessTokenRefreshToken: String!) {
          refreshAccessToken(refreshToken: $refreshAccessTokenRefreshToken) {
            accessToken
            status
          }
        }
      `,
      variables: { refreshAccessTokenRefreshToken: refreshToken },
    })
    .then(res => {
      const newAccessToken = res.data?.refreshAccessToken?.accessToken;
      setStorageData(STORAGE_CONTANTS.AUTHTOKEN, newAccessToken, true);
      return newAccessToken;
    });

  return newToken;
};

const apolloHttpLink = createHttpLink({
  uri: process.env.REACT_APP_API_URL,
});

const apolloAuthLink = setContext(async (request, { headers }) => {
  if (request.operationName !== 'RefreshToken') {
    let token = getStorageData(STORAGE_CONTANTS.AUTHTOKEN);

    const shouldRefresh = isRefreshNeeded(token);

    if (token && shouldRefresh.needRefresh) {
      const refreshPromise = await refreshAuthToken();

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

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

  return { headers };
});

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

【讨论】:

    【解决方案5】:

    一个更简单的解决方案是使用 RetryLink。 retryIf supports async operations 所以可以这样做:

    class GraphQLClient {
      constructor() {
        const httpLink = new HttpLink({ uri: '<graphql-endpoint>', fetch: fetch })
        const authLink = setContext((_, { headers }) => this._getAuthHeaders(headers))
        const retryLink = new RetryLink({
          delay: { initial: 300, max: Infinity, jitter: false },
          attempts: {
            max: 3,
            retryIf: (error, operation) => this._handleRetry(error, operation)
        }})
        
        this.client = new ApolloClient({
          link: ApolloLink.from([ authLink, retryLink, httpLink ]),
          cache: new InMemoryCache()
        })
      }
    
      async _handleRetry(error, operation) {
        let requiresRetry = false
              
        if (error.statusCode === 401) {
            requiresRetry = true
            if (!this.refreshingToken) {
              this.refreshingToken = true
              await this.requestNewAccessToken()
              operation.setContext(({ headers = {} }) => this._getAuthHeaders(headers))
              this.refreshingToken = false
            }
        }
    
        return requiresRetry
      }
    
      async requestNewAccessToken() {
        // get new access token
      }
    
      _getAuthHeaders(headers) {
        // return headers 
      }
    }
    

    【讨论】:

      猜你喜欢
      • 2020-08-25
      • 1970-01-01
      • 2021-10-31
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-12-15
      • 2016-08-25
      • 2018-09-29
      相关资源
      最近更新 更多