我正在使用 Angular 17 和 NgRx 模式。我正在尝试实现一个拦截器,以将正确的标头附加到向后端发出的请求。
具体来说,根据 REST API 模型,遵循的逻辑如下:
除了“登录”、“注册”、“重置密码”之外的所有调用都必须正确设置标头 Authorization、Accept-Language 和 Timezone-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);
})
);
}
}
刷新令牌调用仅在令牌过期时更新数据库。
这是服务的后端代码(在 .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;
}