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

Posted

技术标签:

【中文标题】使用 graphql 和 apollo 客户端刷新 Angular 令牌【英文标题】:refresh token for angular using graphql and apollo client 【发布时间】:2020-08-25 03:36:55 【问题描述】:

当我的第一个请求返回 401 时,我正在尝试设置刷新令牌策略以使用 GraphQL 和 apollo 客户端刷新 angular 9 中的 JWT。

我已经为 graphql 设置了一个新的 angular 模块,我正在其中创建我的 apolloclient。即使使用经过身份验证的请求,一切都很好,但我需要让我的正常刷新令牌策略也能正常工作(刷新令牌周期完成后重新制作并返回原始请求)。我只找到了一些资源来帮助解决这个问题,而且我已经非常接近了——我唯一缺少的是从我的刷新令牌 observable 中返回 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';

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  

我知道我的请求正在第二次工作,因为如果我从我的 authenticationService 订阅中的 forward(operation) observable 中注销结果,我可以在最初的 401 失败后看到结果。

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

上面显示了来自原始请求的数据,但它没有被传递给我最初调用 graphql 的组件。

我远非 observables 专家,但我想我需要做一些地图(平面地图、合并地图等)以使这个返回正常工作,但我只是不知道。

任何帮助将不胜感激

TIA

编辑#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))
        );
      
    
  );

我现在看到这个错误被抛出:

core.js:6210 错误类型错误:您在预期流的位置提供了无效对象。您可以提供 Observable、Promise、Array 或 Iterable。

编辑 #2:包括 onError() 函数签名的屏幕截图:

编辑#3 这是最终的工作解决方案,以防其他人遇到此问题并需要它来获得角度。我不喜欢更新我的服务方法来返回一个承诺,然后将该承诺转换为一个 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  

【问题讨论】:

嗨,我遇到了循环依赖问题,因为 AuthService 中有 GqlApis 调用,而 GqlModule 使用 AuthService。你是怎么解决的? @All2Pie 在这里也一样!你解决了吗? @Lindeberg 是的,检查我的答案。 【参考方案1】:

我对 GraphQL 不太熟悉,但我认为这应该可以正常工作:

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

另外,如果您想了解mergeMap(和concatMap)的工作原理,可以查看this answer。

switchMap 只保留一个活动的内部 observable,一旦外部值进入,当前的内部 observable 将被取消订阅,并根据新到达的外部值和提供的函数创建一个新的。

【讨论】:

感谢您的回复。我尝试了这个(并将它与我的 httpinterceptor 服务进行了比较),因为我正在使用 switchMap,正如你所展示的那样,它在 onError() 上下文中不起作用。它实际上并没有在我的 authenticationservice 类中订阅我的 observable,因为我已经使用tap() 登录了那里的输出...不过,这在我前面提到的 httpinterceptor 中确实有效。我认为这是我的 map() 方法中的问题。我现在正在调查。 出于某种原因,我没有注意到所有事情都发生在 arr.map(...) 中;我不确定errorLink 内部应该发生什么,但我认为您将 cb 传递给map 以返回该 observable(如答案所示)。嗯,所以你有一系列错误。如果有更多,应该优先考虑哪一个? 我越来越近了。请参阅我在 OP 中的编辑。我认为我不需要在这里诚实地使用map()(至少在这种情况下不需要)。这是onError()apollographql.com/docs/link/links/error的文档 所以errorLink 应该返回一个apollo-link Observable,而不是RxJs 的。 你.... 是...... 吓坏了.... 太棒了.... 我已经为此奋斗了两天,你让我到了那里。非常感谢。我将在 OP 中发布我更新的工作解决方案,以供其他需要 Angular 的人使用。【参考方案2】:

这是我的实现,供以后看到的任何人使用

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 

用户服务/认证服务:

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



【讨论】:

如何避免循环依赖?您在 AppModule 中导入 GraphQLModule,并且您的服务提供在:root 中。使用此配置,我得到“错误:NG0200:检测到 DI 中的循环依赖”。能给点建议吗?

以上是关于使用 graphql 和 apollo 客户端刷新 Angular 令牌的主要内容,如果未能解决你的问题,请参考以下文章

使用 Django、GraphQL、Apollo 和 VueJS 进行 URL 管理

使用 Express-GraphQL 和 React-Apollo 订阅 GraphQL

Apollo+React:如何使用代理使客户端和 graphql 在同一个域上?

在 reactJS 中使用动态字段的 Apollo 客户端 graphql 查询

GraphQL + Apollo 请求

Apollo 客户端和 graphQl 查询错误:“OrganizationWhereUniqueInput”类型的变量“$where”预期值