Angular 中的刷新令牌

无需不断登录即可维护用户会话是顺畅的 Web 体验的关键。在本博客中,我将向您展示如何在 Angular 中实现令牌刷新工作流、处理 401 错误并有效管理并发请求。

什么是刷新令牌工作流?

在身份验证系统中,访问令牌的有效期较短,以最大限度地降低安全风险。当访问令牌过期时,刷新令牌允许应用程序从服务器请求新的访问令牌,而无需用户再次登录。

角度实施

我们将使用 Angular 的 `HttpInterceptor` 实现刷新令牌机制。目标是拦截未经授权的请求(401 错误)并在重试原始请求之前刷新令牌。

完整的工作流程

  • 请求拦截:拦截器检测到 401 未授权响应。
  • Token 刷新:如果 token 已过期,refreshToken 将获取新的 token。
  • 重试请求:使用新令牌重试原始请求。
  • 队列管理:一旦令牌刷新,待处理的请求就会被处理。
  • refresh token workflow

    代码概述

  • 令牌刷新逻辑当请求由于令牌过期而失败时,handleUnauthorized 方法处理令牌刷新。
  • handleUnauthorized(
      req: HttpRequest,
      next: HttpHandlerFn
    ): Observable {
      if (!this.isRefreshingToken) {
        this.isRefreshingToken = true;
    
        // Notify all waiting requests that the token is being refreshed
        this.tokenSubject.next(null);
    
        return this.refreshToken().pipe(
          switchMap((newToken: string) => {
            if (newToken) {
              this.tokenSubject.next(newToken);
              // Retry the original request with the new token
              return next(this.addToken(req, newToken));
            }
    
            // If token refresh fails, log out the user
            this.logout();
            return throwError(() => 'Token expired');
          }),
          catchError((error) => {
            this.logout(); // Log out on error
            return throwError(() => error);
          }),
          finalize(() => {
            this.isRefreshingToken = false; // Reset the flag
          }),
        );
      } else {
        // Queue requests while a token is being refreshed
        return this.tokenSubject.pipe(
          filter((token) => token != null),
          take(1),
          switchMap((token) => next(this.addToken(req, token))),
        );
      }
    }

    `handleUnauthorized` 函数旨在管理 HTTP 请求收到 401 Unauthorized 状态的情况,表示访问令牌已过期或无效。此函数可确保应用程序可以刷新令牌并无缝重试失败的请求。

  • 防止多次刷新请求该函数使用 isRefreshingToken 标志来确保每次只发出一个令牌刷新请求。如果令牌已在刷新,则后续请求将排队,直到新令牌可用。
  • if (!this.isRefreshingToken) {
      this.isRefreshingToken = true;
      this.tokenSubject.next(null);
  • 刷新令牌如果没有正在进行的刷新请求,它将使用 refreshToken 方法启动令牌刷新。一旦收到新令牌:
  • 它存储在 tokenSubject 中。
  • 使用更新后的令牌重试原始请求。
  • return this.refreshToken(url).pipe(
      switchMap((newToken: string) => {
        if (newToken) {
          this.tokenSubject.next(newToken);
          return next(this.addToken(req, newToken));
        }
        this.logout();
        return throwError(() => 'Token expired');
      }),
  • 处理并发请求如果令牌刷新已在进行中,则该函数将后续请求排队。这些请求等待 tokenSubject 发出新令牌后再继续。
  • return this.tokenSubject.pipe(
      filter((token) => token != null), // Wait for a non-null token
      take(1), // Only take the first emitted token
      switchMap((token) => next(this.addToken(req, token))),
    );
  • 错误处理如果token刷新失败或者抛出异常:
  • 用户已退出。
  • 向调用者返回一个错误。
  • catchError((error) => {
      this.logout();
      return throwError(() => error);
    }),
  • 清理 finalize 操作符确保重置 isRefreshingToken 标志,从而允许后续刷新请求。
  • finalize(() => {
      this.isRefreshingToken = false;
    }),

    将令牌添加到请求

    addToken 方法将新令牌附加到传出请求的标头。

    addToken(request: HttpRequest, token: string): HttpRequest {
      return request.clone({
        setHeaders: {
          'X-Token': token,
        },
      });
    }

    在 Angular HTTP 拦截器中使用它

    `HttpInterceptor` 是实现此工作流程的理想场所。它允许您拦截所有 HTTP 请求并全局处理令牌管理,而无需修改单个服务调用。

    return next.handle(request).pipe(
          catchError((error) => {
            if (error.status === 401) {
              return this.authService.handleUnauthorized(req, next);
            }
            return throwError(() => error);
          }),
        );

    总而言之,可靠的令牌刷新工作流可确保 Angular 应用程序中的无缝用户体验和安全会话管理。通过有效处理 401 错误并管理并发请求,您可以保持可靠性并让用户满意。感谢您的阅读——欢迎在下面分享您的想法或问题!