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

Angular 17函数式拦截器实现令牌刷新:注入与逻辑整合问题求解

Angular 17 函数式拦截器实现令牌刷新问题

问题背景

之前使用类式拦截器实现了用户令牌刷新功能,代码如下:

@Injectable()
export class AuthInterceptor implements HttpInterceptor {

    constructor(private inject: Injector) {}

    intercept(request: HttpRequest < any > , next: HttpHandler): Observable < HttpEvent < any >> {
        let tokenService = this.inject.get(TokenService);

        let authReq = request;
        authReq = this.AddTokenHeader(request, tokenService.getAccessToken())

        return next.handle(authReq).pipe(
            catchError(errorData => {
                if (errorData.status == 401) {
                    return this.handleRefreshToken(request, next);
                }
                return throwError(errorData);
            })
        );
    }

    AddTokenHeader(request: HttpRequest < any > , token: any) {
        return request.clone({
            headers: request.headers.set('Authorization', 'Bearer ' + token)
        });
    }

    handleRefreshToken(request: HttpRequest < any > , next: HttpHandler) {
        let tokenService = this.inject.get(TokenService);
        let authService = this.inject.get(AuthService);
        let sessionStorageService = this.inject.get(SessionStorageService);

        var refreshTokenVM = new RefreshTokenVM();
        refreshTokenVM.UserId = tokenService.getUserId();
        refreshTokenVM.RefreshToken! = tokenService.getRefreshToken() !;

        return tokenService.refreshToken(refreshTokenVM).pipe(
            switchMap((data: any) => {
                sessionStorageService.saveData('accessToken', data.accessToken);
                sessionStorageService.saveData('refreshToken', data.refreshToken);
                return next.handle(this.AddTokenHeader(request, data.accessToken))
            }),
            catchError(errorData => {
                authService.logout();
                return throwError(errorData);
            })
        );
    }
}

升级到Angular 17后需要转换为函数式拦截器,目前的尝试代码:

export const authInterceptor: HttpInterceptorFn = (request: HttpRequest<unknown>, next: HttpHandlerFn): Observable<HttpEvent<unknown>> => {
  const clonedRequest = request.clone({
    setHeaders: {
      Authorization: 'Bearer ' + 'token',
    }
  });
  return next(clonedRequest).pipe(
    catchError(errorData => {
      if (errorData.status == 401) {

      }
      return throwError(() => errorData);
    })
  );
};

遇到的核心问题:

  • 无法在拦截器函数中注入tokenService(令牌存储在SessionStorage中)
  • 不清楚如何将handleRefreshToken逻辑整合到函数式拦截器中

解决方案

1. 在函数式拦截器中注入服务

Angular 17的函数式拦截器可以直接通过inject()函数获取依赖实例,无需构造函数或Injector。只需在拦截器函数内部调用inject(服务类)即可:

import { inject } from '@angular/core';
// 导入你的服务
import { TokenService } from './path/to/token.service';
import { AuthService } from './path/to/auth.service';
import { SessionStorageService } from './path/to/session-storage.service';

export const authInterceptor: HttpInterceptorFn = (request: HttpRequest<unknown>, next: HttpHandlerFn): Observable<HttpEvent<unknown>> => {
  // 直接注入所需服务
  const tokenService = inject(TokenService);
  const authService = inject(AuthService);
  const sessionStorageService = inject(SessionStorageService);

  // ...后续逻辑
};

2. 整合令牌刷新逻辑

将原handleRefreshToken的逻辑内联到拦截器的catchError流程中,或封装为内部函数,利用已注入的服务完成刷新:

完整的函数式拦截器代码如下:

import { inject } from '@angular/core';
import { HttpInterceptorFn, HttpRequest, HttpHandlerFn, HttpEvent } from '@angular/common/http';
import { Observable, throwError, switchMap, catchError } from 'rxjs';
import { TokenService } from './path/to/token.service';
import { AuthService } from './path/to/auth.service';
import { SessionStorageService } from './path/to/session-storage.service';
import { RefreshTokenVM } from './path/to/refresh-token-vm';

export const authInterceptor: HttpInterceptorFn = (request: HttpRequest<unknown>, next: HttpHandlerFn): Observable<HttpEvent<unknown>> => {
  const tokenService = inject(TokenService);
  const authService = inject(AuthService);
  const sessionStorageService = inject(SessionStorageService);

  // 封装添加Token头的工具函数
  const addTokenHeader = (req: HttpRequest<unknown>, token: string): HttpRequest<unknown> => {
    return req.clone({
      headers: req.headers.set('Authorization', `Bearer ${token}`)
    });
  };

  // 给初始请求添加Token
  const authReq = addTokenHeader(request, tokenService.getAccessToken());

  return next(authReq).pipe(
    catchError(errorData => {
      // 处理401未授权场景
      if (errorData.status === 401) {
        // 构建刷新Token请求体
        const refreshTokenVM = new RefreshTokenVM();
        refreshTokenVM.UserId = tokenService.getUserId();
        refreshTokenVM.RefreshToken = tokenService.getRefreshToken()!;

        // 发起刷新Token请求
        return tokenService.refreshToken(refreshTokenVM).pipe(
          switchMap((data: any) => {
            // 更新存储的令牌
            sessionStorageService.saveData('accessToken', data.accessToken);
            sessionStorageService.saveData('refreshToken', data.refreshToken);
            // 用新Token重新发起原请求
            return next(addTokenHeader(request, data.accessToken));
          }),
          catchError(refreshError => {
            // 刷新失败,执行登出逻辑
            authService.logout();
            return throwError(() => refreshError);
          })
        );
      }
      // 其他错误直接抛出
      return throwError(() => errorData);
    })
  );
};

3. 额外优化建议

  • 确保所有注入的服务都添加了@Injectable({ providedIn: 'root' })装饰器,这样才能通过inject()正确获取实例。
  • 避免并发刷新请求:如果多个请求同时返回401,会触发多次刷新接口调用。可以在TokenService中添加一个状态Subject来控制刷新状态,示例如下:
// 在TokenService中添加
import { BehaviorSubject } from 'rxjs';

private isRefreshing$ = new BehaviorSubject<boolean>(false);

// 在拦截器中判断
if (tokenService.isRefreshing$.value) {
  return tokenService.isRefreshing$.pipe(
    filter(isRefreshing => !isRefreshing),
    take(1),
    switchMap(() => next(addTokenHeader(request, tokenService.getAccessToken())))
  );
}
// 刷新开始时设置isRefreshing$.next(true),结束后设置为false

内容的提问来源于stack exchange,提问作者Alex Wright

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.01 19:04:57