angular - 刷新令牌请求被发送两次 - 寻找除 HttpBackend 之外的另一种解决方案
问题描述
我正在编辑整个问题以简化它,因为我发现了它的原因。如果您查看下面的屏幕截图,“授权属性已应用”被调用了两次。next.handle(...)
创建一个订阅,我不确定,但我auth.service.ts
创建了另一个订阅(第二个)而不是返回Observable<HttpEvent<any>>
. 我认为这才是真正的问题。我发现了一个类似的问题Angular HTTP Interceptor subscribing to observable and then returned next.handle but throwing TypeError: You provided 'undefined',但不幸的是它并没有真正帮助我解决我的问题。
return this.authService.currentUser$.pipe(
switchMap(currentUser => {
console.log('authorize attribute applied');
const headers = request.headers.set('Authorization', `Bearer ${currentUser.accessToken}`);
return next.handle(request.clone({ headers }));
})
);
auth.interceptor.ts
import { Injectable } from '@angular/core';
import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http';
import { Observable, switchMap } from 'rxjs';
import { AuthService } from '@modules/auth/auth.service';
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
constructor(private authService: AuthService) {}
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
console.log('auth interceptor called');
if (!request.headers.has('Content-Type')) {
request = request.clone({
headers: request.headers.set('Content-Type', 'application/json')
});
}
return this.authService.currentUser$.pipe(
switchMap(currentUser => {
console.log('authorize attribute applied');
const headers = request.headers.set('Authorization', `Bearer ${currentUser.accessToken}`);
return next.handle(request.clone({ headers }));
})
);
}
}
auth.service.ts
import { Injectable, OnDestroy } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Router } from '@angular/router';
import {
BehaviorSubject,
delay,
map,
Observable,
of,
Subscription,
switchMap,
tap,
timer
} from 'rxjs';
import { environment } from '@env';
import { JwtService } from '@core/services';
import { AuthResponse, INITIAL_AUTH_STATE, User } from './auth';
@Injectable({
providedIn: 'root'
})
export class AuthService implements OnDestroy {
private readonly TOKEN_URL = `${environment.apiUrl}/Accounts/token`;
private currentUserSubject = new BehaviorSubject<AuthResponse>(INITIAL_AUTH_STATE);
private timer!: Subscription;
currentUser$: Observable<AuthResponse> = this.currentUserSubject.asObservable();
get userInfo(): User | null {
const accessToken = this.currentUserValue?.accessToken;
return accessToken ? this.jwtService.decodeToken<User>(accessToken) : null;
}
private get currentUserValue(): AuthResponse | null {
return this.currentUserSubject.value;
}
private get localStorageCurrentUser(): AuthResponse {
const localStorageUser = localStorage.getItem('currentUser');
return localStorageUser ? JSON.parse(localStorageUser) : INITIAL_AUTH_STATE;
}
constructor(
private httpClient: HttpClient,
private router: Router,
private jwtService: JwtService
) {
this.currentUserSubject.next(this.localStorageCurrentUser);
window.addEventListener('storage', this.storageEventListener.bind(this));
}
ngOnDestroy(): void {
window.removeEventListener('storage', this.storageEventListener.bind(this));
}
signIn(username: string, password: string): Observable<AuthResponse> {
const TOKEN_URL: string = this.TOKEN_URL + '/create';
return this.httpClient
.post<AuthResponse>(TOKEN_URL, {
username,
password
})
.pipe(
map((res) => {
if (res && res.accessToken) {
this.setCurrentUser(res);
}
return res;
})
);
}
signOut(): void {
this.clearCurrentUser();
this.router.navigate(['auth']);
}
refreshToken(): Observable<AuthResponse | null> {
console.log('refresh token call');
const accessToken = this.currentUserValue?.accessToken;
const refreshToken = this.currentUserValue?.refreshToken;
if (!accessToken || !refreshToken) {
this.clearCurrentUser();
return of(null);
}
return this.httpClient
.post<AuthResponse>(`${this.TOKEN_URL}/refresh`, {
accessToken: accessToken,
refreshToken: refreshToken
})
.pipe(
map((res) => {
this.setCurrentUser(res);
return res;
})
);
}
private setCurrentUser(user: AuthResponse) {
this.currentUserSubject.next(user);
this.setLocalStorage(user);
this.startTokenTimer();
}
private clearCurrentUser() {
this.currentUserSubject.next(INITIAL_AUTH_STATE);
this.clearLocalStorage();
this.stopTokenTimer();
}
private setLocalStorage(userState: AuthResponse) {
localStorage.setItem('currentUser', JSON.stringify(userState));
localStorage.setItem('login-event', 'login' + Math.random());
}
private clearLocalStorage() {
localStorage.removeItem('currentUser');
localStorage.setItem('logout-event', 'logout' + Math.random());
}
private getTokenRemainingTime(): number {
const expiresAtUtc = this.currentUserValue?.expiresAtUtc;
if (!expiresAtUtc) {
return 0;
}
const expires = new Date(expiresAtUtc);
return expires.getTime() - Date.now();
}
private startTokenTimer() {
console.log('timeout called');
const timeout = this.getTokenRemainingTime();
this.timer = of(true)
.pipe(
delay(timeout),
tap(() => this.refreshToken().subscribe())
)
.subscribe();
}
private stopTokenTimer() {
this.timer?.unsubscribe();
}
private storageEventListener(event: StorageEvent) {
if (event.storageArea === localStorage) {
if (event.key === 'logout-event') {
this.currentUserSubject.next(INITIAL_AUTH_STATE);
}
if (event.key === 'login-event') {
location.reload();
}
}
}
}
编辑:
我确认问题是我上面描述的问题。基本上next.handle(...)
被调用了两次,两次刷新令牌请求是因为拦截器创建了一个新订阅并auth.service.ts
创建了另一个订阅。我应该以某种方式重新使用第一个订阅,但我不知道该怎么做。
第一种方式
有一种解决方法可以修复双重刷新令牌请求,但我不喜欢它,因为next.handle(...)
它仍然被调用两次。
import { Injectable, OnDestroy } from '@angular/core';
import { HttpBackend, HttpClient } from '@angular/common/http';
import { Router } from '@angular/router';
import {
BehaviorSubject,
delay,
map,
Observable,
of,
Subscription,
switchMap,
tap,
timer
} from 'rxjs';
import { environment } from '@env';
import { JwtService } from '@core/services';
import { AuthResponse, INITIAL_AUTH_STATE, User } from './auth';
@Injectable({
providedIn: 'root'
})
export class AuthService implements OnDestroy {
private readonly TOKEN_URL = `${environment.apiUrl}/Accounts/token`;
private currentUserSubject = new BehaviorSubject<AuthResponse>(INITIAL_AUTH_STATE);
private timer!: Subscription;
private backendClient: HttpClient;
currentUser$: Observable<AuthResponse> = this.currentUserSubject.asObservable();
get userInfo(): User | null {
const accessToken = this.currentUserValue?.accessToken;
return accessToken ? this.jwtService.decodeToken<User>(accessToken) : null;
}
private get currentUserValue(): AuthResponse | null {
return this.currentUserSubject.value;
}
private get localStorageCurrentUser(): AuthResponse {
const localStorageUser = localStorage.getItem('currentUser');
return localStorageUser ? JSON.parse(localStorageUser) : INITIAL_AUTH_STATE;
}
constructor(
private httpClient: HttpClient,
private router: Router,
private jwtService: JwtService,
handler: HttpBackend
) {
this.currentUserSubject.next(this.localStorageCurrentUser);
window.addEventListener('storage', this.storageEventListener.bind(this));
this.backendClient = new HttpClient(handler);
}
ngOnDestroy(): void {
window.removeEventListener('storage', this.storageEventListener.bind(this));
}
signIn(username: string, password: string): Observable<AuthResponse> {
const TOKEN_URL: string = this.TOKEN_URL + '/create';
return this.httpClient
.post<AuthResponse>(TOKEN_URL, {
username,
password
})
.pipe(
map((res) => {
if (res && res.accessToken) {
this.setCurrentUser(res);
}
return res;
})
);
}
signOut(): void {
this.clearCurrentUser();
this.router.navigate(['auth']);
}
refreshToken(): Observable<AuthResponse | null> {
console.log('refresh token call');
const accessToken = this.currentUserValue?.accessToken;
const refreshToken = this.currentUserValue?.refreshToken;
if (!accessToken || !refreshToken) {
this.clearCurrentUser();
return of(null);
}
return this.backendClient
.post<AuthResponse>(`${this.TOKEN_URL}/refresh`, {
accessToken: accessToken,
refreshToken: refreshToken
})
.pipe(
map((res) => {
this.setCurrentUser(res);
return res;
})
);
}
private setCurrentUser(user: AuthResponse) {
this.currentUserSubject.next(user);
this.setLocalStorage(user);
this.startTokenTimer();
}
private clearCurrentUser() {
this.currentUserSubject.next(INITIAL_AUTH_STATE);
this.clearLocalStorage();
this.stopTokenTimer();
}
private setLocalStorage(userState: AuthResponse) {
localStorage.setItem('currentUser', JSON.stringify(userState));
localStorage.setItem('login-event', 'login' + Math.random());
}
private clearLocalStorage() {
localStorage.removeItem('currentUser');
localStorage.setItem('logout-event', 'logout' + Math.random());
}
private getTokenRemainingTime(): number {
const expiresAtUtc = this.currentUserValue?.expiresAtUtc;
if (!expiresAtUtc) {
return 0;
}
const expires = new Date(expiresAtUtc);
return expires.getTime() - Date.now();
}
private startTokenTimer() {
console.log('timeout called');
const timeout = this.getTokenRemainingTime();
this.timer = of(true)
.pipe(
delay(timeout),
tap(() => this.refreshToken().subscribe())
)
.subscribe();
}
private stopTokenTimer() {
this.timer?.unsubscribe();
}
private storageEventListener(event: StorageEvent) {
if (event.storageArea === localStorage) {
if (event.key === 'logout-event') {
this.currentUserSubject.next(INITIAL_AUTH_STATE);
}
if (event.key === 'login-event') {
location.reload();
}
}
}
}
第二种方式
这个解决了这两个问题,但单独订阅,只是为了 currentUser$ Observable?
import { Injectable, OnDestroy } from '@angular/core';
import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http';
import { Observable, Subject, switchMap, takeUntil } from 'rxjs';
import { AuthService } from '@modules/auth/auth.service';
@Injectable()
export class AuthInterceptor implements HttpInterceptor, OnDestroy {
private componentDestroyed$ = new Subject<boolean>();
constructor(private authService: AuthService) {}
ngOnDestroy(): void {
this.componentDestroyed$.next(true);
this.componentDestroyed$.complete();
}
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
console.log('auth interceptor called');
this.authService.currentUser$.pipe(takeUntil(this.componentDestroyed$)).subscribe((user) => {
const isLoggedIn = user && user.accessToken;
if (isLoggedIn) {
request = request.clone({
setHeaders: { Authorization: `Bearer ${user.accessToken}` }
});
}
return next.handle(request);
});
return next.handle(request);
}
}
解决方案
您订阅了两次 - 自然会访问服务器两次。
用这个管道替换有问题的代码部分:
import { timer } from 'rxjs';
import { switchMap } from 'rxjs/operators';
// ...
this.timer = timer(timeout)
.pipe(switchMap(() => this.refreshToken())
.subscribe()
推荐阅读
- airflow - 气流:Jinja 模板未在 subdag pythonOperator 中呈现
- linux - ambari_agent 重启导致 ansible 崩溃
- digital-ocean - 是否可以使用 url 令牌访问私有文件?
- pgbouncer - 配置为 dockerized postgres 时,pgbouncer 无法启动
- javascript - 阅读更多按钮脚本不适用于 html 格式
- google-apps-script - 从播放列表中删除重复的视频
- django - django:用 filter() 或 only() 替换 values() 但在同一行求和
- vue.js - 是否可以将 ipcRenderer 收到的消息保存在电子中以在 ipcRenderer 范围之外使用?
- python - Python有没有类似于java的缓存线程池?
- magento2 - 如何在没有 API 的情况下计算 UPS Shipping Zone