Angular+Parse+Keycloak通用化处理无效令牌错误方案咨询
最优方案:使用HTTP拦截器全局处理无效令牌错误
针对你的场景,**HTTP拦截器(HttpInterceptor)**是解决无效令牌错误通用化处理的最优方案,它能全局捕获所有HTTP请求错误,避免重复逻辑,同时通过延迟注入解决循环依赖问题。以下是具体实现步骤和代码示例:
1. 实现认证拦截器
创建拦截器统一处理209(无效会话令牌)错误,自动触发重新认证并重试请求,失败则执行登出逻辑:
import { Injectable, Injector } from '@angular/core'; import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor, HttpErrorResponse } from '@angular/common/http'; import { Observable, throwError, from } from 'rxjs'; import { catchError, switchMap, retry } from 'rxjs/operators'; import { AuthService } from './auth.service'; @Injectable() export class AuthInterceptor implements HttpInterceptor { // 用Injector延迟获取AuthService,避免循环依赖 private authService: AuthService; constructor(private injector: Injector) {} intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> { // 延迟初始化AuthService,规避循环依赖 if (!this.authService) { this.authService = this.injector.get(AuthService); } return next.handle(request).pipe( catchError((error: HttpErrorResponse) => { // 仅处理Parse的无效会话令牌错误(code=209) if (error.error?.code === 209) { // 调用重新认证方法,将Promise转为Observable return from(this.authService.authBack()).pipe( switchMap((success: boolean) => { if (success) { // 重新认证成功,重试原请求 return next.handle(request); } else { // 认证失败,执行登出跳转 this.authService.logout(); this.authService.login(); return throwError(() => new Error('重新认证失败')); } }), // 仅重试1次,避免无限循环 retry(1), catchError((retryError) => { // 重试仍失败,强制登出 this.authService.logout(); this.authService.login(); return throwError(() => retryError); }) ); } // 其他错误直接抛出,由业务代码自行处理 return throwError(() => error); }) ); } }
2. 注册拦截器
在AppModule中注册拦截器,使其全局生效:
import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; import { AppComponent } from './app.component'; import { AuthInterceptor } from './auth.interceptor'; @NgModule({ declarations: [AppComponent], imports: [BrowserModule, HttpClientModule], providers: [ { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true } ], bootstrap: [AppComponent] }) export class AppModule { }
3. 改造通用资源服务(RessourceService)
移除服务内的错误处理逻辑,改用拦截器全局处理;同时通过Injector延迟获取AuthService,解决循环依赖:
import { Injector } from '@angular/core'; import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; import { environment } from '../environments/environment'; import { AuthService } from './auth.service'; export abstract class RessourceService<T> { protected readonly ressourceUrl: string; abstract getRessourceName(): string; readonly headers: HttpHeaders; protected readonly defaultRessourcePath = 'classes/'; private authService: AuthService; constructor( protected httpClient: HttpClient, private injector: Injector, ressourcePath?: string ) { this.authService = this.injector.get(AuthService); this.ressourceUrl = `${environment.back.url}/parse/${ ressourcePath ?? this.defaultRessourcePath }${this.getRessourceName()}`; this.headers = new HttpHeaders({ 'X-Parse-Application-Id': environment.back.appId, 'Content-Type': 'application/json', }); } getList(includes?: string): Observable<Array<T>> { const url = includes ? `${this.ressourceUrl}?include=${includes}` : this.ressourceUrl; return this.httpClient .get<ListResponse<T>>(url, { headers: this.getHeaders() }) .pipe(map((list) => list.results)); // 移除原有的catchError,交给拦截器处理 } // 获取带会话令牌的请求头 protected getHeaders(): HttpHeaders { const sessionToken = this.authService.getParseSessionToken(); return sessionToken ? this.headers.set('X-Parse-Session-Token', sessionToken) : this.headers; } } // 辅助接口 interface ListResponse<T> { results: T[]; }
4. 更新业务服务(如CarService)
修改构造函数,传入Injector以适配父类:
import { Injectable, Injector } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { RessourceService } from './ressource.service'; import { Car } from './car.model'; @Injectable({ providedIn: 'root' }) export class CarService extends RessourceService<Car> { getRessourceName(): string { return 'Car'; } constructor(protected httpClient: HttpClient, private injector: Injector) { super(httpClient, injector); } }
5. 简化组件代码
组件无需再处理错误逻辑,拦截器会自动完成认证重试和登出跳转:
ngAfterViewInit(): void { this.carService.getList().subscribe((res: Array<Car>) => { this.carList = res; }); // 错误已由拦截器全局处理,无需编写冗余逻辑 }
6. 完善AuthService(关键方法)
确保authBack方法返回Promise,供拦截器转换为Observable处理:
import { Injectable } from '@angular/core'; import { KeycloakService } from 'keycloak-angular'; import { UserService } from './user.service'; @Injectable({ providedIn: 'root' }) export class AuthService { constructor( private keycloak: KeycloakService, private userService: UserService ) {} // 重新认证后端(返回Promise,便于拦截器转换为Observable) async authBack(): Promise<boolean> { try { // 从Keycloak获取最新令牌 const keycloakToken = await this.keycloak.getToken(); // 调用Parse接口更新会话令牌 const parseSession = await this.userService.loginWithKeycloak(keycloakToken).toPromise(); // 存储新的会话令牌 localStorage.setItem('parseSessionToken', parseSession.sessionToken); return true; } catch (error) { return false; } } // 获取存储的Parse会话令牌 getParseSessionToken(): string | null { return localStorage.getItem('parseSessionToken'); } logout(): void { localStorage.removeItem('parseSessionToken'); this.keycloak.logout(); } login(): void { this.keycloak.login(); } }
方案优势
- 全局统一处理:所有HTTP请求的无效令牌错误都由拦截器处理,彻底消除代码冗余
- 解决循环依赖:通过
Injector延迟注入AuthService,避免服务间循环依赖问题 - 自动化流程:自动触发重新认证、请求重试,失败后自动登出跳转,无需手动干预
- 逻辑清晰:错误处理逻辑集中在拦截器,业务代码更简洁
内容的提问来源于stack exchange,提问作者ScorprocS
相关产品推荐
相关产品推荐

