使用 HttpInterceptor 解决 Angular 中的 JWT 刷新令牌处理问题

Temp mail SuperHeros
使用 HttpInterceptor 解决 Angular 中的 JWT 刷新令牌处理问题
使用 HttpInterceptor 解决 Angular 中的 JWT 刷新令牌处理问题

确保角度拦截器中的无缝 JWT 刷新

在具有安全用户会话的 Web 应用程序中,有效管理短期 JWT 令牌对于不间断的用户体验至关重要。当令牌过期时,用户经常会遇到被迫重新登录等问题,这可能会令人沮丧并破坏用户参与度。为了解决这个问题,开发人员通常使用 Angular 拦截器来处理过期会话来实现“自动令牌刷新”。 🕰️

此方法涉及拦截 HTTP 请求、捕获 401 错误(未经授权的请求),然后调用刷新过程以获取新令牌。但是,在确保将更新的令牌或 cookie 应用于重试的请求时可能会出现问题。如果新令牌未正确传播,重试可能会失败,从而使用户遇到相同的授权错误,并可能会中断应用程序工作流程。

在本指南中,我们将逐步介绍此拦截器模式的实际实现。我们将了解如何捕获错误、刷新令牌以及确认请求以有效授权重试。这种方法可以最大程度地减少中断,同时让您能够控制会话更新过程。

最后,您将深入了解如何解决常见陷阱,例如处理 HttpOnly cookie 以及在高请求量期间管理刷新序列。此方法可确保您的应用程序无需持续登录即可维持安全、流畅的用户会话。 🔒

命令 使用示例
catchError 在 Observable 管道中用于捕获和处理 HTTP 请求期间发生的错误,允许拦截器拦截 401 错误,专门用于刷新令牌或处理未经授权的请求。
switchMap 切换到新的可观察量,通常在此处用于处理刷新令牌后的 HTTP 重试。通过切换流,它替换了先前的可观察值,确保仅处理带有新令牌的重试请求。
BehaviorSubject 专门的 RxJS 主题,用于维护跨 HTTP 请求的令牌刷新状态。与常规主题不同,BehaviorSubject 保留最后发出的值,有助于处理并发 401 错误。
clone 使用更新的属性(如 withCredentials: true)克隆 HttpRequest 对象。这允许 cookie 与请求一起发送,同时保留原始请求配置。
pipe 在一个 Observable 中将多个 RxJS 运算符链接在一起。在此拦截器中,管道对于在令牌刷新后编写错误处理和重试逻辑至关重要。
of 一个 RxJS 实用程序,可根据值创建可观察值。在测试中,of(true)用于模拟refreshToken的成功响应,有助于拦截器的单元测试。
HttpTestingController Angular 测试模块中的实用程序,允许在测试环境中拦截和控制 HTTP 请求。它有助于模拟响应并断言拦截器已正确处理请求。
flush 与 HttpTestingController 一起使用,在测试中手动完成 HTTP 请求,允许模拟 401 Unauthorized 等响应。这确保拦截器的刷新逻辑按预期激活。
getValue 访问BehaviorSubject的当前值,这对于验证令牌刷新过程是否已经在进行中、避免多次刷新请求的拦截器来说至关重要。

使用 Angular 拦截器确保可靠的 JWT 身份验证

在上面的示例中,拦截器设计为每当遇到 401 错误时自动刷新短暂的 JWT 令牌。这种设置对于具有敏感数据的应用程序至关重要,其中维护会话安全至关重要,但用户体验不应中断。拦截器捕获 401(未经授权)错误并发起刷新令牌请求以更新会话,而不需要用户重新进行身份验证。此过程由 catchError 函数触发,该函数允许在可观察管道内进行错误处理。在这里,任何 HTTP 错误,特别是 401,都表明令牌可能已过期并启动刷新过程。

switchMap 函数是这里的另一个核心元素;它为刷新的请求创建一个新的可观察流,替换旧的可观察流,而不取消整个流。刷新后,它会重试原始请求,确保应用新令牌。通过从旧的可观察值切换到新的可观察值,拦截器可以以无缝、非阻塞的方式执行令牌更新。该技术在处理实时应用程序时特别有价值,因为它可以减少用户交互的中断,同时仍然保持安全身份验证。例如,浏览安全财务仪表板的用户不会被重定向或不必要地注销;相反,新令牌会在后台获取并应用。 🔄

此外,BehaviorSubject 通过管理刷新过程的状态发挥着至关重要的作用。这个 RxJS 实用程序可以保留最后发出的值,这在多个请求同时遇到 401 错误时特别有用。拦截器不会触发多次刷新,而是仅发起一次令牌刷新,所有其他请求都会排队等待这一单个令牌更新。将BehaviorSubject 与switchMap 结合使用有助于确保如果一个请求触发刷新,则需要新令牌的所有其他请求将使用更新后的凭据,而不会导致重复的刷新调用。当用户可能打开多个选项卡或应用程序同时管理多个网络调用时,此功能非常有用,从而节省资源并避免过多的服务器负载。

测试此拦截器逻辑对于确保其在不同场景下工作也至关重要,这就是我们包含 HttpTestingController 的原因。这个 Angular 测试工具使我们能够在受控环境中模拟和测试 HTTP 响应,例如 401 未经授权状态。使用 HttpTestingController 提供的刷新方法,开发人员可以模拟真实世界的错误响应并验证拦截器的行为是否符合预期。这种测试方法使我们能够在部署应用程序之前改进刷新逻辑处理各种情况的能力。通过这些方法,拦截器不仅可以安全地保留会话,还可以为用户导航应用程序提供更加无缝、稳定的体验。 👩‍💻

使用 Angular 实现 JWT 拦截器:错误处理和刷新令牌解决方案

使用 Angular 和模块化服务结构进行错误处理和会话管理

import { Injectable } from '@angular/core';
import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest, HttpErrorResponse } from '@angular/common/http';
import { catchError, switchMap } from 'rxjs/operators';
import { Observable, throwError, BehaviorSubject } from 'rxjs';
import { AuthService } from './auth.service';
import { Router } from '@angular/router';
@Injectable()
export class JwtInterceptor implements HttpInterceptor {
  private refreshTokenInProgress$ = new BehaviorSubject<boolean>(false);
  constructor(private authService: AuthService, private router: Router) {}
  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    req = req.clone({ withCredentials: true });
    return next.handle(req).pipe(
      catchError((error: HttpErrorResponse) => {
        if (error.status === 401) {
          return this.handle401Error(req, next);
        }
        return throwError(() => error);
      })
    );
  }
  private handle401Error(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    if (!this.refreshTokenInProgress$.getValue()) {
      this.refreshTokenInProgress$.next(true);
      return this.authService.refreshToken().pipe(
        switchMap(() => {
          this.refreshTokenInProgress$.next(false);
          return next.handle(req.clone({ withCredentials: true }));
        }),
        catchError((error) => {
          this.refreshTokenInProgress$.next(false);
          this.authService.logout();
          this.router.navigate(['/login'], { queryParams: { returnUrl: req.url } });
          return throwError(() => error);
        })
      );
    }
    return this.refreshTokenInProgress$.pipe(
      switchMap(() => next.handle(req.clone({ withCredentials: true })))
    );
  }
}

JWT 拦截器令牌刷新处理的 Angular 单元测试

在 Angular 拦截器中测试 JWT 刷新和 HTTP 错误处理

import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { JwtInterceptor } from './jwt.interceptor';
import { HTTP_INTERCEPTORS, HttpClient } from '@angular/common/http';
import { AuthService } from './auth.service';
describe('JwtInterceptor', () => {
  let httpMock: HttpTestingController;
  let authServiceSpy: jasmine.SpyObj<AuthService>;
  let httpClient: HttpClient;
  beforeEach(() => {
    authServiceSpy = jasmine.createSpyObj('AuthService', ['refreshToken', 'logout']);
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],
      providers: [
        JwtInterceptor,
        { provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true },
        { provide: AuthService, useValue: authServiceSpy }
      ]
    });
    httpMock = TestBed.inject(HttpTestingController);
    httpClient = TestBed.inject(HttpClient);
  });
  afterEach(() => {
    httpMock.verify();
  });
  it('should refresh token on 401 error and retry request', () => {
    authServiceSpy.refreshToken.and.returnValue(of(true));
    httpClient.get('/test').subscribe();
    const req = httpMock.expectOne('/test');
    req.flush(null, { status: 401, statusText: 'Unauthorized' });
    expect(authServiceSpy.refreshToken).toHaveBeenCalled();
  });
});

使用 Angular 拦截器扩展 JWT 令牌刷新策略

使用 Angular 的一个关键方面 JWT 令牌拦截器 对于安全应用程序来说,有效地处理管理身份验证和会话过期的复杂性。除了捕获 401 错误和刷新令牌之外,还必须考虑多请求处理以及如何优化令牌刷新。当多个请求同时遇到 401 错误时,实现队列或锁定机制对于确保一次仅发生一个令牌刷新非常有用。这种方法可以防止不必要的 API 调用并减少负载,特别是在高流量应用程序中,同时允许所有排队的请求在刷新后继续进行。

Angular 的拦截器还允许我们简化处理令牌存储和检索的方式。最好使用 Angular 的,而不是在本地存储中硬编码令牌 仅 Http cookie 和CSRF保护以增强安全性。使用 HttpOnly cookie,JWT 无法通过 JavaScript 访问或操作,这极大地提高了安全性,但也带来了新的挑战:确保请求自动获取刷新的 cookie。 Angular 的内置 withCredentials option 是一种解决方案,指示浏览器在每个请求中包含这些 cookie。

在生产环境中,建议对应用程序在令牌刷新负载下的行为进行性能测试。测试设置可以模拟高请求量,确保拦截器的逻辑有效扩展。实际上,此设置可以最大限度地降低令牌相关错误影响用户体验的风险。拦截器策略与适当的 cookie 处理和测试相结合,有助于维护无缝、用户友好且安全的应用程序 — 无论该应用程序管理关键的财务数据还是社交平台的用户会话。 🌐🔐

有关使用 Angular 拦截器处理 JWT 令牌的常见问题

  1. 怎么样 catchError 帮助 JWT 令牌处理?
  2. 使用 catchError 拦截器允许我们识别 401 错误并在令牌过期时无缝触发令牌刷新请求。
  3. 为什么是 BehaviorSubject 使用而不是 Subject 用于跟踪刷新状态?
  4. BehaviorSubject 保留最后发出的值,使其可用于管理并发请求之间的刷新状态,而无需触发多个刷新调用。
  5. 有什么作用 switchMap 重试 HTTP 请求有何作用?
  6. switchMap 允许从令牌刷新 observable 切换到重试的 HTTP 请求,确保只有最新的 observable 完成。
  7. 如何在 Angular 中测试拦截器?
  8. 角的 HttpTestingController 对于模拟 HTTP 响应(包括 401 错误)非常有用,以验证拦截器逻辑是否正常工作。
  9. 为什么使用 withCredentials 在克隆的请求中?
  10. withCredentials 标志确保每个请求中都包含安全的 HttpOnly cookie,这对于维护安全会话非常重要。
  11. 如何在大流量下优化令牌刷新处理?
  12. 使用单个 BehaviorSubject 或锁定机制可以帮助防止多个刷新请求,从而在高流量场景中提高性能。
  13. 拦截器如何影响会话过期时的用户体验?
  14. 拦截器支持自动会话更新,因此用户不会意外注销,从而提供更流畅的用户体验。
  15. 怎么样 clone 帮助修改请求?
  16. clone 创建具有修改属性的请求副本,例如设置 withCredentials,不改变原来的请求。
  17. 拦截器是否适用于多个用户会话?
  18. 是的,但每个会话需要独立管理其 JWT,或者刷新逻辑应适应多个会话。
  19. 拦截器可以处理非401错误吗?
  20. 是的,拦截器可以扩展以捕获其他错误,例如 403 Forbidden,并适当处理它们以获得更好的用户体验。

简化 Angular 应用程序中的 JWT 令牌刷新

有效的 JWT 令牌管理对于增强 Angular 应用程序的用户体验和安全性至关重要。通过实施拦截器来捕获 401 错误并自动启动令牌刷新,您可以避免强制注销并提供无缝的用户流程。此外,在刷新期间处理并发请求 行为主体,确保仅进行一次刷新调用,从而优化资源使用。

最终的目标是在安全性和用户便利性之间取得平衡。定期测试和完善现实场景的拦截器逻辑,使您的应用程序能够毫无问题地处理大量请求。采用令牌管理的最佳实践有助于在会话中保持安全、用户友好的体验。 👨‍💻

JWT 拦截器实现的参考和资源
  1. 关于在 Angular 中创建 HTTP 拦截器的详细信息可以在 Angular 官方文档中找到: Angular HTTP 指南
  2. 有关管理 JWT 令牌刷新机制和最佳实践的见解,请参阅 Auth0 的刷新令牌指南
  3. RxJS 库提供了有关本文中使用的运算符的大量详细信息,包括 切换映射捕获错误RxJS 操作指南
  4. 对于 Angular 测试策略 Http测试控制器,检查 Angular 测试实用程序上的资源: Angular HTTP 测试指南