简体   繁体   English

使用 graphql 和 apollo 客户端刷新 angular 的令牌

[英]refresh token for angular using graphql and apollo client

I'm trying to set up a refresh token strategy to refresh JWT in angular 9 with GraphQL and apollo client when my first request returns a 401.我正在尝试设置刷新令牌策略以在我的第一个请求返回 401 时使用 GraphQL 和阿波罗客户端刷新 angular 9 中的 JWT。

I have set up a new angular module for graphql where I'm creating my apolloclient.我已经为 graphql 设置了一个新的 angular 模块,我正在创建我的 apolloclient。 Everything works great even with authenticated requests but I need to have my normal refresh token strategy work as well (re-make and return the original request after refresh token cycle completes).即使使用经过身份验证的请求,一切都很好,但我需要让我的正常刷新令牌策略也能正常工作(刷新令牌周期完成后重新制作并返回原始请求)。 I have found only a few resources to help with this and I've gotten really close - the only thing I'm missing is returning the observable from my refresh token observable.我只找到了一些资源来帮助解决这个问题,而且我已经非常接近了——我唯一缺少的是从我的刷新令牌 observable 中返回 observable。

Here is the code that would think should work:这是认为应该工作的代码:

    import { NgModule } from '@angular/core';
import { HttpLinkModule, HttpLink } from 'apollo-angular-link-http';
import { AuthenticationService } from './authentication/services/authentication.service';
import { ApolloLink } from 'apollo-link';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { ApolloModule, APOLLO_OPTIONS } from 'apollo-angular';
import { onError } from 'apollo-link-error';

export function createApollo(httpLink: HttpLink, authenticationService: AuthenticationService) {

  const authLink = new ApolloLink((operation, forward) => {
    operation.setContext({
      headers: {
        Authorization: 'Bearer ' + localStorage.getItem('auth_token')
      }
    });
    return forward(operation);
  });

  const errorLink = onError(({ forward, graphQLErrors, networkError, operation }) => {
    if (graphQLErrors) {
      graphQLErrors.map(({ message, locations, path }) =>
        {
         if (message.toLowerCase() === 'unauthorized') {
          authenticationService.refreshToken().subscribe(() => {
            return forward(operation);
          });
         }
        }
      );
    }
  });

  return {
    link: errorLink.concat(authLink.concat(httpLink.create({ uri: 'http://localhost:3000/graphql' }))),
    cache: new InMemoryCache(),
  };
}


@NgModule({
  exports: [ApolloModule, HttpLinkModule],
  providers: [
    {
      provide: APOLLO_OPTIONS,
      useFactory: createApollo,
      deps: [HttpLink, AuthenticationService]
    }
  ]
})
export class GraphqlModule { }

I know that my request is working the second time because if I log out the result from the forward(operation) observable inside my authenticationService subscription, I can see the results after the initial 401 failure.我知道我的请求第二次起作用了,因为如果我从我的 authenticationService 订阅中的 forward(operation) observable 中注销结果,我可以在最初的 401 失败后看到结果。

 if (message.toLowerCase() === 'unauthorized') {
  authenticationService.refreshToken().subscribe(() => {
    return forward(operation).subscribe(result => {
      console.log(result);
    });
  });
 }

the above shows me the data from the original request, but it's not being passed up to my component that originally called the graphql.上面显示了来自原始请求的数据,但它没有传递给我最初称为 graphql 的组件。

I'm far from an expert with observables but I'm thinking I need to do some kind of map (flatmap, mergemap etc) to make this return work correctly, but I just don't know.我远不是可观察的专家,但我认为我需要做某种 map(平面图、合并图等)以使此返回正常工作,但我只是不知道。

Any help would be greatly appreciated任何帮助将不胜感激

TIA TIA

EDIT #1: this is getting me closer as it's now actually subscribing to my method in AuthenticationService (I see results in the tap())编辑#1:这让我更接近了,因为它现在实际上订阅了我在 AuthenticationService 中的方法(我在 tap() 中看到了结果)

    const errorLink = onError(({ forward, graphQLErrors, networkError, operation }) => {
    if (graphQLErrors) {
      if (graphQLErrors[0].message.toLowerCase() === 'unauthorized') {
        return authenticationService.refreshToken()
        .pipe(
          switchMap(() => forward(operation))
        );
      }
    }
  });

I'm now seeing this error being thrown:我现在看到这个错误被抛出:

core.js:6210 ERROR TypeError: You provided an invalid object where a stream was expected. core.js:6210 错误类型错误:您提供了一个无效的 object,其中预期为 stream。 You can provide an Observable, Promise, Array, or Iterable.您可以提供 Observable、Promise、Array 或 Iterable。

EDIT #2: Including screenshot of onError() function signature:编辑#2:包括 onError() function 签名的截图: 在此处输入图像描述

EDIT #3 Here is the final working solution in case someone else comes across this and needs it for angular.编辑#3 这是最终的工作解决方案,以防其他人遇到此问题并需要它用于 angular。 I don't love having to update my service method to return a promise, and then convert that promise into an Observable - but as @Andrei Gătej discovered for me, this Observable is from a different namespace.我不喜欢更新我的服务方法来返回 promise,然后将 promise 转换为 Observable - 但正如 @Andrei Gătej 为我发现的那样,这个 Observable 来自不同的命名空间。

import { NgModule } from '@angular/core';
import { HttpLinkModule, HttpLink } from 'apollo-angular-link-http';
import { AuthenticationService } from './authentication/services/authentication.service';
import { ApolloLink } from 'apollo-link';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { ApolloModule, APOLLO_OPTIONS } from 'apollo-angular';
import { onError } from 'apollo-link-error';
import { Observable } from 'apollo-link';


export function createApollo(httpLink: HttpLink, authenticationService: AuthenticationService) {

  const authLink = new ApolloLink((operation, forward) => {
    operation.setContext({
      headers: {
        Authorization: 'Bearer ' + localStorage.getItem('auth_token')
      }
    });
    return forward(operation);
  });

  const errorLink = onError(({ forward, graphQLErrors, networkError, operation }) => {
    if (graphQLErrors) {
      if (graphQLErrors.some(x => x.message.toLowerCase() === 'unauthorized')) {
        return promiseToObservable(authenticationService.refreshToken().toPromise()).flatMap(() => forward(operation));
      }
    }
  });

  return {
    link: errorLink.concat(authLink.concat(httpLink.create({ uri: '/graphql' }))),
    cache: new InMemoryCache(),
  };
}

const promiseToObservable = (promise: Promise<any>) =>
    new Observable((subscriber: any) => {
      promise.then(
        value => {
          if (subscriber.closed) {
            return;
          }
          subscriber.next(value);
          subscriber.complete();
        },
        err => subscriber.error(err)
      );
    });


@NgModule({
  exports: [ApolloModule, HttpLinkModule],
  providers: [
    {
      provide: APOLLO_OPTIONS,
      useFactory: createApollo,
      deps: [HttpLink, AuthenticationService]
    }
  ]
})
export class GraphqlModule { }

I'm not quite familiar with GraphQL, but I think this should work fine:我对 GraphQL 不太熟悉,但我认为这应该可以正常工作:

if (message.toLowerCase() === 'unauthorized') {
return authenticationService.refreshToken()
  .pipe(
    switchMap(() => forward(operation))
  );
}

Also, if you'd like to know about how mergeMap (and concatMap ) work, you can have a look at this answer .此外,如果您想了解mergeMap (和concatMap )如何工作,您可以查看这个答案

switchMap keeps only one active inner observable and as soon as an outer value comes in, the current inner observable will be unsubscribed and a new one will be created, based on the newly arrived outer value and the provided function. switchMap只保留一个活动的内部 observable,一旦外部值进入,当前的内部 observable 将被取消订阅,并根据新到达的外部值和提供的 function 创建一个新的内部 observable。

Here is my implementation for anyone seeing this in future这是我为将来看到这个的人的实现

Garaphql Module: Garaphql 模块:

import { NgModule } from '@angular/core';
import { APOLLO_OPTIONS } from 'apollo-angular';
import {
  ApolloClientOptions,
  InMemoryCache,
  ApolloLink,
} from '@apollo/client/core';
import { HttpLink } from 'apollo-angular/http';
import { environment } from '../environments/environment';
import { UserService } from './shared/services/user.service';
import { onError } from '@apollo/client/link/error';
import { switchMap } from 'rxjs/operators';

const uri = environment.apiUrl;

let isRefreshToken = false;
let unHandledError = false;

export function createApollo(
  httpLink: HttpLink,
  userService: UserService
): ApolloClientOptions<any> {
  const auth = new ApolloLink((operation, forward) => {
    userService.user$.subscribe((res) => {
      setTokenInHeader(operation);
      isRefreshToken = false;
    });

    return forward(operation);
  });

  const errorHandler = onError(
    ({ forward, graphQLErrors, networkError, operation }): any => {
      if (graphQLErrors && !unHandledError) {
        if (
          graphQLErrors.some((x) =>
            x.message.toLowerCase().includes('unauthorized')
          )
        ) {
          isRefreshToken = true;

          return userService
            .refreshToken()
            .pipe(switchMap((res) => forward(operation)));
        } else {
          userService.logOut('Other Error');
        }

        unHandledError = true;
      } else {
        unHandledError = false;
      }
    }
  );

  const link = ApolloLink.from([errorHandler, auth, httpLink.create({ uri })]);

  return {
    link,
    cache: new InMemoryCache(),
    connectToDevTools: !environment.production,
  };
}

function setTokenInHeader(operation) {
  const tokenKey = isRefreshToken ? 'refreshToken' : 'token';
  const token = localStorage.getItem(tokenKey) || '';
  operation.setContext({
    headers: {
      token,
      Accept: 'charset=utf-8',
    },
  });
}

@NgModule({
  providers: [
    {
      provide: APOLLO_OPTIONS,
      useFactory: createApollo,
      deps: [HttpLink, UserService],
    },
  ],
})
export class GraphQLModule {}

UserService/AuthService:用户服务/身份验证服务:

import { BehaviorSubject, Observable, throwError } from 'rxjs';
import { User, RefreshTokenGQL } from '../../../generated/graphql';
import jwt_decode from 'jwt-decode';
import { Injectable, Injector } from '@angular/core';
import { Router } from '@angular/router';
import { catchError, tap } from 'rxjs/operators';
import { AlertService } from './alert.service';

@Injectable({
  providedIn: 'root',
})
export class UserService {
  private userSubject: BehaviorSubject<User>;
  public user$: Observable<User>;

  constructor(
    private router: Router,
    private injector: Injector,
    private alert: AlertService
  ) {
    const token = localStorage.getItem('token');
    let user;
    if (token && token !== 'undefined') {
      try {
        user = jwt_decode(token);
      } catch (error) {
        console.log('error', error);
      }
    }
    this.userSubject = new BehaviorSubject<User>(user);
    this.user$ = this.userSubject.asObservable();
  }

  setToken(token?: string, refreshToken?: string) {
    let user;

    if (token) {
      user = jwt_decode(token);
      localStorage.setItem('token', token);
      localStorage.setItem('refreshToken', refreshToken);
    } else {
      localStorage.removeItem('token');
      localStorage.removeItem('refreshToken');
    }

    this.userSubject.next(user);
    return user;
  }

  logOut(msg?: string) {
    if (msg) {
      this.alert.addInfo('Logging out...', msg);
    }

    this.setToken();
    this.router.navigateByUrl('/auth/login');
  }

  getUser() {
    return this.userSubject.value;
  }

  refreshToken() {
    const refreshTokenMutation = this.injector.get<RefreshTokenGQL>(
      RefreshTokenGQL
    );

    return refreshTokenMutation.mutate().pipe(
      tap(({ data: { refreshToken: res } }) => {
        this.setToken(res.token, res.refreshToken);
      }),
      catchError((error) => {
        console.log('On Refresh Error: ', error);
        this.logOut('Session Expired, Log-in again');
        return throwError('Session Expired, Log-in again');
      })
    );
  }
}


声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM