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
相关产品推荐
相关产品推荐

