You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Angular 9中GraphQL与Apollo Client的JWT刷新令牌策略问题

我完全理解你对当前将Observable转成Promise再转回Observable的方案的不满——这确实是绕了个没必要的弯子,咱们可以直接用RxJS的原生操作符来实现整个刷新令牌的流程,让代码更简洁、更符合Angular和Apollo的生态习惯。

核心优化思路

你的authenticationService.refreshToken()本身应该返回的是RxJS Observable(Angular的HTTP请求默认都是Observable),所以我们可以直接通过RxJS的操作符串联刷新令牌和重发请求的逻辑,不需要任何Promise转换。

优化后的完整代码

首先确保导入必要的RxJS操作符:

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';
// 导入RxJS操作符
import { switchMap, tap } from 'rxjs/operators';

export function createApollo(httpLink: HttpLink, authenticationService: AuthenticationService) {
  const authLink = new ApolloLink((operation, forward) => {
    // 每次请求都从localStorage获取最新的token
    operation.setContext({
      headers: {
        Authorization: 'Bearer ' + localStorage.getItem('auth_token')
      }
    });
    return forward(operation);
  });

  const errorLink = onError(({ forward, graphQLErrors, operation }) => {
    // 检查是否存在未授权错误
    const hasUnauthorizedError = graphQLErrors?.some(error => 
      error.message.toLowerCase() === 'unauthorized'
    );

    if (hasUnauthorizedError) {
      // 直接用refreshToken的Observable,串联重发请求的逻辑
      return authenticationService.refreshToken().pipe(
        // 刷新成功后,更新当前请求的Authorization头(确保用新token)
        tap(() => {
          operation.setContext({
            headers: {
              Authorization: 'Bearer ' + localStorage.getItem('auth_token')
            }
          });
        }),
        // 重发原请求
        switchMap(() => forward(operation))
      );
    }

    // 如果不是未授权错误,让错误继续传播
    return;
  });

  return {
    // 注意link的顺序:errorLink应该在authLink之前,这样错误能被捕获处理
    link: errorLink.concat(authLink.concat(httpLink.create({ uri: '/graphql' }))),
    cache: new InMemoryCache(),
  };
}

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

额外优化:防止重复刷新令牌

如果同时有多个请求返回401,上面的代码可能会触发多次刷新令牌请求,这显然不是我们想要的。可以在AuthenticationService里添加一个状态跟踪,让所有等待刷新的请求共享同一个刷新操作:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { BehaviorSubject, Observable, filter, finalize, switchMap, take, tap } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class AuthenticationService {
  // 跟踪刷新令牌的状态:true表示正在刷新
  private isRefreshing$ = new BehaviorSubject<boolean>(false);

  constructor(private http: HttpClient) {}

  refreshToken(): Observable<any> {
    // 如果正在刷新,等待刷新完成后再继续
    if (this.isRefreshing$.value) {
      return this.isRefreshing$.pipe(
        filter(isRefreshing => !isRefreshing),
        take(1),
        switchMap(() => this.performRefreshToken())
      );
    }

    // 标记开始刷新
    this.isRefreshing$.next(true);

    return this.performRefreshToken().pipe(
      // 无论成功失败,都标记刷新结束
      finalize(() => this.isRefreshing$.next(false))
    );
  }

  // 实际执行刷新令牌的私有方法
  private performRefreshToken(): Observable<any> {
    const refreshToken = localStorage.getItem('refresh_token');
    
    return this.http.post('/api/refresh-token', { refreshToken }).pipe(
      tap((response: any) => {
        // 更新localStorage里的accessToken和refreshToken(如果后端返回新的)
        localStorage.setItem('auth_token', response.accessToken);
        if (response.refreshToken) {
          localStorage.setItem('refresh_token', response.refreshToken);
        }
      })
    );
  }
}

为什么这个方案更好?

  • 无冗余转换:完全基于RxJS Observable链式调用,避免了Promise和Observable之间的来回转换,代码更简洁。
  • 可扩展性强:利用RxJS的操作符,可以轻松添加错误重试、防重复、日志记录等额外逻辑。
  • 符合生态习惯:和Angular、Apollo Client的设计理念一致,后续维护和调试更方便。

内容的提问来源于stack exchange,提问作者Blair Holmes

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.07 15:12:52