在 Angular 中使用刷新令牌拦截器和 NgRx 模式时的并发问题

问题描述 投票:0回答:1

我正在使用 Angular 17 和 NgRx 模式。我正在尝试实现一个拦截器,以将正确的标头附加到向后端发出的请求。

具体来说,根据 REST API 模型,遵循的逻辑如下:

除了“登录”、“注册”、“重置密码”之外的所有调用都必须正确设置标头 AuthorizationAccept-LanguageTimezone-Offset。 除了“刷新令牌”端点之外的所有调用,如果令牌已过期,必须先调用“刷新令牌”端点,并使用收到的新令牌更新存储。 我遇到了什么问题?似乎存在并发或同步问题,可能是由调度新令牌引起的。发生的情况是,由于同时进行多个调用,并且所有调用都需要在过期时首先刷新令牌,我发现自己在商店中保存了一个令牌,该令牌与刷新后保存在数据库中的令牌不同步。因此,调用“refresh-token”端点会导致“400 - Session Not Found”错误,从而阻止任何进一步的调用。

我尝试实现请求队列但没有成功。

这是代码:


@Injectable()
export class HttpHeaderRequestInterceptor implements HttpInterceptor {
  constructor(
    private _accountAPI: AccountAPI,
    private _store: Store,
    private _authUtils: AuthUtils
  ) {}

  intercept(
    request: HttpRequest<any>,
    next: HttpHandler
  ): Observable<HttpEvent<any>> {
    const refreshEndpoint = 'refresh-token';
    const excludedEndpoints = ['login', 'register', 'reset-password'];

    if (request.url.includes(refreshEndpoint)) {
      // For "refresh-token" endpoint, set only the Authorization header
      return this.setAuthorizationHeader(request, next);
    } else if (
      excludedEndpoints.some((endpoint) => request.url.includes(endpoint)) ||
      !request.url.includes('api')
    ) {
      // For "login", "register", or "reset-password" endpoints, do not modify the request
      return next.handle(request);
    } else {
      // For other endpoints, call refreshToken API and modify the request
      return this.refreshTokenAndContinue(request, next);
    }
  }

  private setAuthorizationHeader(
    request: HttpRequest<any>,
    next: HttpHandler
  ): Observable<HttpEvent<any>> {
    return from(this._store.select(selectAuthToken)).pipe(
      take(1),
      switchMap((token) => {
        const updatedRequest = request.clone({
          setHeaders: {
            Authorization: `Bearer ${token}`,
          },
        });

        return next.handle(updatedRequest);
      })
    );
  }

  private refreshTokenAndContinue(
    request: HttpRequest<any>,
    next: HttpHandler
  ): Observable<HttpEvent<any>> {
    const refreshToken$ = this._accountAPI.refreshToken();
    const currentLang$ = this._store.select(selectLangState);
    return combineLatest([refreshToken$, currentLang$]).pipe(
      take(1),
      switchMap(([tokenResponse, lang]) => {
        const tokenExists = tokenResponse.headers.get('Authorization');
        const token = tokenExists ? tokenExists.split(' ')[1].trim() : '';
        const tokenExpiration =
          this._authUtils.getAuthTokenExpirationDate(token);
        this._store.dispatch(
          accountActions.refreshTokenSuccess({
            authToken: token,
            tokenExpiration,
          })
        );

        const timeZoneOffset = new Date().getTimezoneOffset().toString();
        const updatedRequest = request.clone({
          setHeaders: {
            Authorization: `Bearer ${token}`,
            'Accept-Language': lang || DEFAULT_LANG,
            'Timezone-Offset': timeZoneOffset,
          },
        });

        return next.handle(updatedRequest);
      })
    );
  }
}

angular ngrx refresh-token angular-http-interceptors
1个回答
0
投票

刷新令牌调用仅在令牌过期时更新数据库。

这是服务的后端代码(在 .NET 中):

   public string RefreshToken(string token)
    {
        Log.Information("AuthenticationService - RefreshToken");

        if (string.IsNullOrEmpty(token))
        {
            throw new AppostoException(_localizer["sessionNotFound"]);
        }

        EntityResult<TokenAccount> tokenAccount = TokenUtil.GetTokenAccount(token);
        if (!tokenAccount.IsCorrect)
        {
            throw new AppostoException(_localizer["sessionNotFound"]);
        }

        ExpressionStarter<Account> predicates = PredicateBuilder
            .New<Account>()
            .And(account => account.Id == tokenAccount.Entity.Id);

        Account? account = _dynamicRepository.FindEntity(predicates);
        if (account == null || account.DateDelete != null || !account.Active || ("Bearer " + account.Token) != token)
        {
            throw new AppostoException(_localizer["sessionNotFound"]);
        }

        if (!tokenAccount.Entity.IsExpired && tokenAccount.Entity.TokenValid)
        {
            return token;
        }

        EntityResult<string> newToken = TokenUtil.GetJwtToken(tokenAccount.Entity.Id, tokenAccount.Entity.Role, tokenAccount.Entity.User, tokenAccount.Entity.ImageLink, _jwtSettings);
        if (!newToken.IsCorrect)
        {
            throw new AppostoException(newToken.Errors);
        }

        account.Token = newToken.Entity;
        account.DateUpdate = DateTime.UtcNow;
        account.LastLogin = DateTime.UtcNow;

        _dynamicRepository.Update(account);
        _dynamicRepository.SaveChanges();

        return "Bearer " + newToken.Entity;
    }
© www.soinside.com 2019 - 2024. All rights reserved.