我需要相关建议来实现我的需求,这就是:
我在我的 Angular 应用程序中实现了基于 jwt 的身份验证系统。当我获取新令牌和刷新令牌时,我创建一个 setTimeout ,以便可以在令牌过期之前刷新令牌。但当我引导应用程序(在我的 AppComponent 中)时,我也特意刷新了我的令牌,我的问题与此案例相关。
我从jwt获取登录的用户信息,问题是当应用程序启动并执行令牌刷新时,令牌在组件显示后存储,这导致应用程序获取用户信息之前的令牌而不是新创建的令牌。
我们可以考虑使用解析器,但问题是,使用解析器时,我将触发 2 个刷新令牌查询,一个在我的 AppComponent 中,一个在解析器内,这不是我需要的行为。
所以基本上,这是我的 AppComponent 的一部分:
ngOnInit() {
this.refreshToken();
...
}
private refreshToken() {
this.authSvc.refreshToken().pipe(
untilDestroyed(this),
catchError(error => {
if (error.status === 401) {
this.router.navigate(['/login']);
}
return of();
})
).subscribe();
}
这是AuthService的一部分:
get user(): User | null {
const token = localStorage.getItem('token');
return token ? this.jwtHelper.decodeToken(token) : null;
}
refreshToken(): Observable<LoginResponse> {
return this.http.post<LoginResponse>(
`${environment.apiUrl}/token/refresh`,
{refresh_token: localStorage.getItem('refresh_token')}, this.CONTEXT
).pipe(
tap((res: LoginResponse) => this.storeTokens(res as LoginSuccess))
);
}
storeTokens(data: LoginSuccess): void {
localStorage.setItem('token', data.token);
localStorage.setItem('refresh_token', data.refresh_token);
this.scheduleTokenRefresh(data.token);
}
这是我需要用户数据的组件(它不是唯一需要此数据的组件):
export class HomeComponent implements OnInit {
user!: User | null;
constructor(private authSvc: AuthService) {
}
ngOnInit() {
this.user = this.authSvc.user;
}
我面临的问题是 Home 组件在 storeTokens 被调用之前显示,因此如果自最后一个令牌以来用户数据已在后端更新,我的 HomeComponent 将不知道它,因为它将使用前一个令牌...
我尝试使用解析器错误,它迫使我再次调用refreshToken,但这不是我想要的。我需要将刷新令牌保留在 AppComponent 中,而不是调用它两次..这是解析器:
export const tokenRefreshedResolver: ResolveFn<boolean> = (route, state) => {
return inject(AuthService).refreshToken().pipe(
map(() => true),
catchError(() => of(false))
);
};
什么是合适的解决方案?
我强烈建议您使用 APP_INITIALIZER 提供程序来处理此问题。如果您不熟悉它,这里是文档,这是我的简短解释。
APP_INITIALIZER 可用于在应用程序引导之前执行所需的功能。这意味着您应该能够在任何内容开始渲染之前获取令牌,从而避免出现问题。这比在 AppComponent 中处理要好得多。
您需要在 AppModule 中添加 APP_INITIALIZER 提供程序,然后按照您想要的方式实现它。常见的解决方案是使用工厂函数。
// AppModule
import { APP_INITIALIZER, NgModule } from '@angular/core';
import { AuthService } from 'somewhere/in/you/project';
@NgModule({
declarations: [
AppComponent, AboutUsComponent,HomeComponent,ContactUsComponent
],
imports: [
HttpClientModule,
BrowserModule,
AppRoutingModule,
],
// Add a custom provider
providers: [
{
provide: APP_INITIALIZER,
useFactory: appInit, // function returning a Promise or Observable
deps: [AuthService] // dependencies needed for the factory func
}
],
bootstrap: [AppComponent]
})
export class AppModule { }
实现工厂功能。将其放入单独的文件中或作为 NgModule 声明上方的函数。主要取决于它能达到多大。
// Factory method file
import { AuthService } from 'somewhere/in/you/project';
// Deps defined in AppModule reflect into the parameters of your factory function.
// You can add more if needed.
export function appInit(
authService: AuthService,
): () => Observable<boolean> {
return authService.refreshToken().pipe(
map(response => !!response) // Map it in some way that it returns boolean.
// Check if response exists, or contains some property, etc.
)
}
大多数 AUTH 库都推荐这种方法。我已经将它与 KeyCloak 和 Azure 一起使用,用户体验非常流畅。但您可能会遇到的情况是,加载时间有点长。但直接在 index.html 中实现一些加载屏幕非常简单