Как реализовать токен автоматического обновления в graphql для аутентификации на основе jwt?

Я пытаюсь выяснить этот сценарий для моей аутентификации на основе JWT в сервере graphql на основе Apollo (2.0) .

в основном после входа в систему пользователь получает accessToken и refreshToken с сервера.

AccessToken истекает через определенный период времени, и сервер отправляет сообщение об ошибке, указывающее, что токен истек (TokenExpiredError), а затем клиенту необходимо связаться с сервером для нового доступа через передачу refreshToken.

поток следующий -

  1. TokenExpiredError происходит
  2. получить эту ошибку на стороне клиента
  3. очередь все запросы со старым accessToken(так что сервер не затоплен слишком много вызовов refreshToken и многие accessTokens генерируются сервером)
  4. вызовите api refreshToken на сервере graphql, чтобы получить новый accessToken
  5. обновление accessToken для всех разрешенных вызовов с новыми маркер accessToken
  6. пользователь выхода из системы, если сам refreshToken истек
  7. предотвратить любой вид гонки состояние B / w звонки

Я уже реализовал мутацию refreshToken на стороне клиента, но не могу понять, когда происходит ошибка остановить все запросы - > запросить новый токен - > сделать все отложенные запросы снова, и если токен обновления истек пользователь выхода из системы.

2 ответов


вы можете использовать сервис auth0 jwl, доступный из https://github.com/auth0/angular2-jwt

используя эту функцию, вы можете реализовать http inteceptor для беспрепятственного управления связью. Допустимый пример можно найти в этом потоке: https://github.com/auth0/angular2-jwt/issues/15

(ищите последний комментарий ZeBigDuck) для удобства я копирую его ответ здесь:

    I did a custom interceptor which handle every request passed after token expiration, and renew it with refresh token before send http requests.

This use redux to handle refreshing state.
And a custom lib that you can easily override or hardcode just to configure some routes.

import { Inject, Injectable } from "@angular/core";
import { HttpClient, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from "@angular/common/http";
import { Router } from "@angular/router";
import { Observable } from "rxjs/Observable";
import { MAGGLE_CONFIG, MaggleConfig } from "../maggle-config";
import { NgRedux } from "@angular-redux/store";
import { IMaggleState } from "../maggle.store";
import { TokenActions } from "../app/token.actions";
import 'rxjs/add/operator/first';
import 'rxjs/add/operator/skip';
import 'rxjs/add/operator/catch';
import { JwtHelperService } from '@auth0/angular-jwt';

@Injectable()
export class RefreshTokenInterceptor implements HttpInterceptor {
  jwtHelper: JwtHelperService;

  constructor(private router: Router, @Inject(MAGGLE_CONFIG) private maggleConfig: MaggleConfig,
              private ngRedux: NgRedux<IMaggleState>, private http: HttpClient, private tokenActions: TokenActions) {
    this.jwtHelper = new JwtHelperService();
  }

  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    const refreshUrl = this.maggleConfig.apiEndpoint + this.maggleConfig.apiRefreshTokenUrl;

    const token = localStorage.getItem('access_token');
    const refreshToken = localStorage.getItem('refresh_token');

    if ((!token && !refreshToken) || request.url === refreshUrl) {
      // continue normally
      return next.handle(request).catch(() => this.redirectToLogin());
    }

    if (this.jwtHelper.isTokenExpired(token)) {
      return this.ngRedux.select<boolean>('refreshingToken').first().flatMap(refreshing => {
        if (refreshing) {

          // Wait for new token before sending the request
          return this.ngRedux.select<boolean>('refreshingToken').skip(1).flatMap(refreshing => {
            return next.handle(request).catch(() => this.redirectToLogin());
          });

        } else {

          // Refresh token if expired and not already refreshing
          this.ngRedux.dispatch(<any>this.tokenActions.refreshToken(true));
          return this.http.post(refreshUrl, {refresh_token: refreshToken}).flatMap(
            res => {
              localStorage.setItem('access_token', res['token']);
              localStorage.setItem('refresh_token', res['refresh_token']);
              this.ngRedux.dispatch(<any>this.tokenActions.refreshToken(false));
              return next.handle(request).catch(() => this.redirectToLogin());
            }).catch(() => this.redirectToLogin());
        }
      });
    } else {
      return next.handle(request);
    }
  }

  redirectToLogin(): Observable<HttpEvent<any>> {
    this.router.navigate(this.maggleConfig.loginRoute);
    localStorage.removeItem('refresh_token');
    localStorage.removeItem('access_token');
    this.ngRedux.dispatch(<any>this.tokenActions.refreshToken(false));
    return Observable.empty();
  }
}

надеюсь, что это помогает.


я следовал этой подход к решению моей проблемы, наконец,

публикация моего подхода для других

// @flow
import { ApolloLink, Observable } from 'apollo-link';
import type { ApolloClient } from 'apollo-client';
import type { Operation, NextLink } from 'apollo-link';

import { refreshToken2, getToken } from './token-service';
import { GraphQLError } from 'graphql';

export class AuthLink extends ApolloLink {
     tokenRefreshingPromise: Promise<boolean> | null;

injectClient = (client: ApolloClient): void => {
    this.client = client;
};

refreshToken = (): Promise<boolean> => {
    //if (!this.tokenRefreshingPromise) this.tokenRefreshingPromise = refreshToken(this.client);
    if (!this.tokenRefreshingPromise) this.tokenRefreshingPromise = refreshToken2();
    return this.tokenRefreshingPromise;
};

setTokenHeader = (operation: Operation): void => {
    const token = getToken();
    if (token) operation.setContext({ headers: { authorization: `Bearer ${token}` } });
};

request(operation: Operation, forward: NextLink) {
    // set token in header
    this.setTokenHeader(operation);
    // try refreshing token once if it has expired
    return new Observable(observer => {
        let subscription, innerSubscription, inner2Subscription;
        try {
            subscription = forward(operation).subscribe({
                next: result => {
                    if (result.errors) {
                        console.log("---->", JSON.stringify(result.errors))
                        for (let err of result.errors) {
                            switch (err.extensions.code) {
                              case 'E140':
                                console.log('E140', result)
                                observer.error(result.errors)
                                break;
                              case 'G130':
                                    this.refreshToken().then(response => {
                                        if (response.data && !response.errors) {
                                            this.setTokenHeader(operation);
                                            innerSubscription = forward(operation).subscribe(observer);
                                        } else {
                                            console.log("After refresh token", JSON.stringify(response));
                                            observer.next(response)
                                        }
                                    }).catch(console.log);
                                break;
                            }
                          }
                    } 
                    observer.next(result)

                  },
                complete: observer.complete.bind(observer),
                error: netowrkError => {
                    observer.error(netowrkError);
                  }
                },
            });
        } catch (e) {
            observer.error(e);
        }
        return () => {
            if (subscription) subscription.unsubscribe();
            if (innerSubscription) innerSubscription.unsubscribe();
            if (inner2Subscription) inner2Subscription.unsubscribe();
        };
    });
}
}