【问题标题】:Error of token refreshing in apollo-angularapollo-angular 中的令牌刷新错误
【发布时间】:2021-03-07 06:26:59
【问题描述】:

我正在使用 apollo-angular 构建一个前端应用程序。在我的应用程序中,我使用 JWT 身份验证系统和短期访问令牌和长期刷新令牌(它们在仅 HTTP cookie 中传输,而不是开始存储在 HTTP 标头中)。

当我运行我的应用程序时,我可以成功登录,但是当访问令牌过期时,我收到以下错误并且在我的浏览器上看不到任何内容。
Error: Network error: Cannot read property 'refresh' of undefined at new ApolloError

我的代码如下:

GraphQLModule(在AppModule中导入)(部分基于this question

import { NgModule } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';

import { ApolloModule, APOLLO_OPTIONS } from 'apollo-angular';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { HttpLinkModule, HttpLink } from 'apollo-angular-link-http';
import { ApolloLink, Observable } from 'apollo-link';
import { onError } from 'apollo-link-error';

import { AuthService } from '../account/auth/auth.service';
import { environment } from '../../environments/environment';

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

const errorLink = onError(({ graphQLErrors, networkError }) => { // need help on linking this with graphql module
  if (graphQLErrors) {
    graphQLErrors.map(({ message, locations, path }) =>
      console.log(
        `[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`,
      ),
    );
  }

  if (networkError) { console.log(`[Network error]: ${networkError}`); }
});

export function createApollo(httpLink: HttpLink, authService: AuthService) {
  const basicLink = httpLink.create({
    uri: environment.apiUrl,
    withCredentials: true,
  });

  const authMiddleware = new ApolloLink((operation, forward) => {
    if (operation.operationName !== 'RefreshToken') {
      if (localStorage.getItem('loginStatus') && localStorage.getItem('loginStatus') === '1') {
        const nowtime = new Date();
        const accessExpiresIn = new Date(localStorage.getItem('accessExpiresIn'));
        const refreshExpiresIn = new Date(localStorage.getItem('refreshExpiresIn'));
        if (accessExpiresIn <= nowtime && refreshExpiresIn > nowtime) {
          return promiseToObservable(authService.refresh().toPromise()).flatMap(() => forward(operation));
        } else if (accessExpiresIn <= nowtime && refreshExpiresIn <= nowtime) {
          return promiseToObservable(authService.logout().toPromise()).flatMap(() => forward(operation));
        } else {
          return forward(operation);
        }
      } else {
        return forward(operation);
      }
    } else {
      return forward(operation);
    }
  });

  const link = ApolloLink.from([errorLink, authMiddleware, basicLink]);
  const cache = new InMemoryCache();

  return {
    link,
    cache,
  };
}

@NgModule({
  imports: [
    HttpClientModule,
    ApolloModule,
    HttpLinkModule
  ],
  exports: [
    ApolloModule,
    HttpLinkModule
  ],
  providers: [
    {
      provide: APOLLO_OPTIONS,
      useFactory: createApollo,
      deps: [HttpLink, AuthService],
    },
  ],
})
export class GraphQLModule { }

AuthService

import { map } from 'rxjs/operators';

import { Injectable } from '@angular/core';
import { Router } from '@angular/router';

import { Apollo } from 'apollo-angular';
import gql from 'graphql-tag';

const login = gql`
  mutation Login($username: String!, $password: String!) {
    getToken (
      email: $username,
      password: $password
    ) {
      payload
      refreshExpiresIn
    }
  }
`;

const refresh = gql`
  mutation RefreshToken {
    refreshToken {
      payload
      refreshExpiresIn
    }
  }
`;

const logout = gql`
  mutation Logout {
    deleteTokenCookie {
      deleted
    }
    deleteRefreshTokenCookie {
      deleted
    }
  }
`;

@Injectable({
  providedIn: 'root',
})
export class AuthService {

  constructor(
    public router: Router,
    private apollo: Apollo,
  ) { }

  saveExpiresIn(accessExpiresIn: number, refreshExpiresIn: number) {
    const accessExpiresIn = new Date(accessExpiresIn * 1000);
    const refreshExpiresIn = new Date(refreshExpiresIn * 1000);
    localStorage.setItem('accessExpiresIn', accessExpiresIn.toString());
    localStorage.setItem('refreshExpiresIn', refreshExpiresIn.toString());
  }

  login(username: string, password: string) {
    return this.apollo.mutate({
      mutation: login,
      variables: {
        username,
        password,
      },
    }).pipe(map((result: any) => {
      this.saveExpiresIn(
        result.data.tokenAuth.payload.exp,
        result.data.tokenAuth.refreshExpiresIn
      );
      localStorage.setItem('loginStatus', String(1));

      return result;
    }));
  }

  refresh() {
    return this.apollo.mutate({
      mutation: refresh
    }).pipe(map((result: any) => {
      this.saveExpiresIn(
        result.data.refreshToken.payload.exp,
        result.data.refreshToken.refreshExpiresIn
      );

      return result;
    }));
  }

  logout() {
    return this.apollo.mutate({
      mutation: logout
    }).pipe(map((result: any) => {
      localStorage.removeItem('loginStatus');
      localStorage.removeItem('accessExpiresIn');
      localStorage.removeItem('refreshExpiresIn');

      return result;
    }));
  }
}

编写这些代码是为了实现以下应用流程:

  1. 用户尝试登录(通过 graphql 突变发送身份验证信息)
  2. 后端服务器将访问令牌和刷新令牌发送到前端应用程序
  3. 用户尝试发送graphql查询,其结果根据用户是否经过身份验证而变化(未经过身份验证的用户也可以看到查询结果)
  4. 前端应用检查用户是否登录以及访问令牌是否过期
  5. 如果用户已登录且访问令牌已过期,则前端应用会发送带有 graphql 突变的刷新令牌,以在原始查询之前获取新的访问令牌
  6. 发回新的访问令牌后发送原始查询

我正在使用 Angular8 和 apollo-angular 1.8.0。

我对 Angular 很陌生,所以我可能会遗漏一些非常简单的东西 ;(
提前谢谢!

【问题讨论】:

标签: angular graphql apollo apollo-angular


【解决方案1】:

不确定确切的解决方案,但您可以尝试使用 catchError RxJS 运算符处理您的 refresh() 方法中的错误 - 也许您需要重试,或者您可以查看有关错误和跟踪的更多详细信息它回来了。

import { catchError, map } from 'rxjs/operators';
import { of } from 'rxjs';

//...

@Injectable({
  providedIn: 'root',
})
export class AuthService {

  //...

  refresh() {
    return this.apollo.mutate({
      mutation: refresh
    }).pipe(
      map((result: any) => {
        if (result) {
          //...
        }
        else {
          console.warn("There was an error. See output above for details.")
        }
      }),
      catchError((error: Error) => {
        // handle the error, maybe retry
        console.log(error && error.message);
        return of(null);
      })
    );
  }

  //...

}

【讨论】:

  • 感谢您的回答。我尝试了您的解决方案(为简单起见,我通过console.log(err); return of() 处理了错误)但仍然发生错误。它没有很好地工作,因为错误没有发生在 refresh() 中,而是在 Apollo 中。
  • 奇怪,Apollo 确实应该将错误发送给处理 - 您是否尝试在 of() 中返回 null ,然后在 map 函数中处理 null ? (更详细地更新了我的答案)
  • 感谢您的回复和修改后的答案。我试过你的解决方案。在我的控制台中,我看到了一个显示 [Network error]: TypeError: Cannot read property 'refresh' of undefined 的日志和一个显示相同内容的错误,但我在浏览器中什么也看不到。
  • 抱歉,我不太了解阿波罗。尽管他们的支持/报告了 git 上的问题,但可能值得一试——如果catchError 块没有捕获到,他们觉得这可能是一个未处理的错误。也就是说,它显然是你的代码中触发它的东西,所以我能建议的最好的方法是删除一些位,直到错误消失 - 这样你就可以确定导致问题的代码的确切行,也许这会给你有更多的见解,或者更具体的问题要问社区?抱歉,我无法提供更多帮助!
  • 不要抱歉,我非常感谢您迄今为止为我所做的一切努力。我实际上是在质疑自己是否在apollo-angular GitHub issue 中发布这个问题。我会在那里寻求帮助。再次非常感谢!
猜你喜欢
  • 2020-08-25
  • 1970-01-01
  • 2021-04-06
  • 2017-10-11
  • 2016-10-14
  • 2021-10-31
  • 2022-11-29
  • 2019-12-01
  • 2020-08-27
相关资源
最近更新 更多