令牌刷新后Angular 4拦截器重试请求


84

嗨,我正在尝试弄清楚如何实现新的角度拦截器并401 unauthorized通过刷新令牌并重试请求来处理错误。这是我一直遵循的指南:https : //ryanchenkie.com/angular-authentication-using-the-http-client-and-http-interceptors

我已经成功地缓存了失败的请求,并且可以刷新令牌,但是我不知道如何重新发送以前失败的请求。我也想使它与我当前使用的解析器一起使用。

令牌拦截器

return next.handle( request ).do(( event: HttpEvent<any> ) => {
        if ( event instanceof HttpResponse ) {
            // do stuff with response if you want
        }
    }, ( err: any ) => {
        if ( err instanceof HttpErrorResponse ) {
            if ( err.status === 401 ) {
                console.log( err );
                this.auth.collectFailedRequest( request );
                this.auth.refreshToken().subscribe( resp => {
                    if ( !resp ) {
                        console.log( "Invalid" );
                    } else {
                        this.auth.retryFailedRequests();
                    }
                } );

            }
        }
    } );

身份验证服务

cachedRequests: Array<HttpRequest<any>> = [];

public collectFailedRequest ( request ): void {
    this.cachedRequests.push( request );
}

public retryFailedRequests (): void {
    // retry the requests. this method can
    // be called after the token is refreshed
    this.cachedRequests.forEach( request => {
        request = request.clone( {
            setHeaders: {
                Accept: 'application/json',
                'Content-Type': 'application/json',
                Authorization: `Bearer ${ this.getToken() }`
            }
        } );
        //??What to do here
    } );
}

上面的retryFailedRequests()文件是我不知道的。重试后,如何重新发送请求并使它们通过解析器可用于路由?

这是所有相关的代码,如果有帮助的话:https : //gist.github.com/joshharms/00d8159900897dc5bed45757e30405f9


3
我有同样的问题,似乎没有答案。
LastTribunal

Answers:


98

我的最终解决方案。适用于并行请求。

更新:使用Angular 9 / RxJS 6更新代码,当refreshToken失败时,错误处理和修复循环

import { HttpRequest, HttpHandler, HttpInterceptor, HTTP_INTERCEPTORS } from "@angular/common/http";
import { Injector } from "@angular/core";
import { Router } from "@angular/router";
import { Subject, Observable, throwError } from "rxjs";
import { catchError, switchMap, tap} from "rxjs/operators";
import { AuthService } from "./auth.service";

export class AuthInterceptor implements HttpInterceptor {

    authService;
    refreshTokenInProgress = false;

    tokenRefreshedSource = new Subject();
    tokenRefreshed$ = this.tokenRefreshedSource.asObservable();

    constructor(private injector: Injector, private router: Router) {}

    addAuthHeader(request) {
        const authHeader = this.authService.getAuthorizationHeader();
        if (authHeader) {
            return request.clone({
                setHeaders: {
                    "Authorization": authHeader
                }
            });
        }
        return request;
    }

    refreshToken(): Observable<any> {
        if (this.refreshTokenInProgress) {
            return new Observable(observer => {
                this.tokenRefreshed$.subscribe(() => {
                    observer.next();
                    observer.complete();
                });
            });
        } else {
            this.refreshTokenInProgress = true;

            return this.authService.refreshToken().pipe(
                tap(() => {
                    this.refreshTokenInProgress = false;
                    this.tokenRefreshedSource.next();
                }),
                catchError(() => {
                    this.refreshTokenInProgress = false;
                    this.logout();
                }));
        }
    }

    logout() {
        this.authService.logout();
        this.router.navigate(["login"]);
    }

    handleResponseError(error, request?, next?) {
        // Business error
        if (error.status === 400) {
            // Show message
        }

        // Invalid token error
        else if (error.status === 401) {
            return this.refreshToken().pipe(
                switchMap(() => {
                    request = this.addAuthHeader(request);
                    return next.handle(request);
                }),
                catchError(e => {
                    if (e.status !== 401) {
                        return this.handleResponseError(e);
                    } else {
                        this.logout();
                    }
                }));
        }

        // Access denied error
        else if (error.status === 403) {
            // Show message
            // Logout
            this.logout();
        }

        // Server error
        else if (error.status === 500) {
            // Show message
        }

        // Maintenance error
        else if (error.status === 503) {
            // Show message
            // Redirect to the maintenance page
        }

        return throwError(error);
    }

    intercept(request: HttpRequest<any>, next: HttpHandler): Observable<any> {
        this.authService = this.injector.get(AuthService);

        // Handle request
        request = this.addAuthHeader(request);

        // Handle response
        return next.handle(request).pipe(catchError(error => {
            return this.handleResponseError(error, request, next);
        }));
    }
}

export const AuthInterceptorProvider = {
    provide: HTTP_INTERCEPTORS,
    useClass: AuthInterceptor,
    multi: true
};

3
@AndreiOstrovski,能否请您使用AuthService的答案imports和代码进行更新?
Takehin

4
我有一种感觉,如果由于某种原因this.authService.refreshToken()失败,则所有等待刷新的并行查询将永远等待。
Maksim Gumerov '17

2
刷新令牌上的陷阱从来不需要我。它击中了可观察的.throw。
jamesmpw

2
伙计们,它适用于并行和顺序请求。您发送5个请求,它们返回401,然后执行1 refreshToken,然后再次发送5个请求。如果您的5个请求是连续的,则在第401个请求之后,我们发送refreshToken,然后再发送第一个请求,再发送其他4个请求。
Andrei Ostrovski

2
如果Angular可以装饰您的服务,为什么要手动注入服务@Injectable()呢?另外,一个catchError不会返回任何内容。至少回来EMPTY
Győri的Sándor

16

使用最新版本的Angular(7.0.0)和rxjs(6.3.3),这就是我创建了功能齐全的Auto Session Recovery拦截器的方式,以确保如果并发请求因401失败,那么它也应该只命中令牌刷新API一次,并使用switchMap和Subject将失败的请求传递给响应。下面是我的拦截器代码的样子。我已经省略了auth服务和store服务的代码,因为它们是非常标准的服务类。

import {
  HttpErrorResponse,
  HttpEvent,
  HttpHandler,
  HttpInterceptor,
  HttpRequest
} from "@angular/common/http";
import { Injectable } from "@angular/core";
import { Observable, Subject, throwError } from "rxjs";
import { catchError, switchMap } from "rxjs/operators";

import { AuthService } from "../auth/auth.service";
import { STATUS_CODE } from "../error-code";
import { UserSessionStoreService as StoreService } from "../store/user-session-store.service";

@Injectable()
export class SessionRecoveryInterceptor implements HttpInterceptor {
  constructor(
    private readonly store: StoreService,
    private readonly sessionService: AuthService
  ) {}

  private _refreshSubject: Subject<any> = new Subject<any>();

  private _ifTokenExpired() {
    this._refreshSubject.subscribe({
      complete: () => {
        this._refreshSubject = new Subject<any>();
      }
    });
    if (this._refreshSubject.observers.length === 1) {
      this.sessionService.refreshToken().subscribe(this._refreshSubject);
    }
    return this._refreshSubject;
  }

  private _checkTokenExpiryErr(error: HttpErrorResponse): boolean {
    return (
      error.status &&
      error.status === STATUS_CODE.UNAUTHORIZED &&
      error.error.message === "TokenExpired"
    );
  }

  intercept(
    req: HttpRequest<any>,
    next: HttpHandler
  ): Observable<HttpEvent<any>> {
    if (req.url.endsWith("/logout") || req.url.endsWith("/token-refresh")) {
      return next.handle(req);
    } else {
      return next.handle(req).pipe(
        catchError((error, caught) => {
          if (error instanceof HttpErrorResponse) {
            if (this._checkTokenExpiryErr(error)) {
              return this._ifTokenExpired().pipe(
                switchMap(() => {
                  return next.handle(this.updateHeader(req));
                })
              );
            } else {
              return throwError(error);
            }
          }
          return caught;
        })
      );
    }
  }

  updateHeader(req) {
    const authToken = this.store.getAccessToken();
    req = req.clone({
      headers: req.headers.set("Authorization", `Bearer ${authToken}`)
    });
    return req;
  }
}

根据@ anton-toshik的评论,我认为在书面形式中解释此代码的功能是个好主意。你可以在我的文章有一个读这里的这段代码的解释和理解(如何和为什么它的工作原理?)。希望能帮助到你。


1
良好的工作,第二个return内部intercept函数应该是这样的:return next.handle(this.updateHeader(req)).pipe(。目前,您仅在刷新身份验证令牌后才发送该身份验证令牌...
malimo

我想我是通过switchmap做到的。请再次检查。如果我误解了您的意思,请告诉我。
Samarpan '19

是的,它基本上可以正常工作,但是您总是发送两次请求-一次不发送标头,然后在发送标头失败后....
malimo

@SamarpanBhattacharya这有效。我认为这个答案可以为像我这样不了解Observable的工作原理的人提供语义上的解释。
安东·东芝

1
@NikaKurashvili,此方法定义对我有用:public refreshToken(){const url:string=environment.apiUrl+API_ENDPOINTS.REFRESH_TOKEN;const req:any={token:this.getAuthToken()};const head={};const header={headers:newHttpHeaders(head)};return this.http.post(url,req,header).pipe(map(resp=>{const actualToken:string=resp['data'];if(actualToken){this.setLocalStorage('authToken',actualToken);}return resp;}));}
Shrinivas

9

我也遇到了类似的问题,我认为收集/重试逻辑过于复杂。相反,我们可以只使用catch运算符来检查401,然后观察令牌刷新,然后重新运行请求:

return next.handle(this.applyCredentials(req))
  .catch((error, caught) => {
    if (!this.isAuthError(error)) {
      throw error;
    }
    return this.auth.refreshToken().first().flatMap((resp) => {
      if (!resp) {
        throw error;
      }
      return next.handle(this.applyCredentials(req));
    });
  }) as any;

...

private isAuthError(error: any): boolean {
  return error instanceof HttpErrorResponse && error.status === 401;
}

1
我喜欢用的498自定义状态代码来识别过期的标记与401这也可以表明没有足够的私法
约瑟夫·卡罗尔

1
嗨,我正在尝试使用return next.handle(reqClode)并没有执行任何操作,我的代码与您的经验不同,但无法正常工作的部分是return部分。authService.createToken(authToken,refreshToken); this.inflightAuthRequest = null; 返回next.handle(req.clone({headers:req.headers.set(appGlobals.AUTH_TOKEN_KEY,authToken)})));

6
收集/重试逻辑并不过于复杂,如果您不想在令牌过期时对refreshToken端点发出多个请求,则必须采用此方法。假设您的令牌已过期,并且您几乎同时发出了5个请求。按照此注释中的逻辑,将在服务器端生成5个新的刷新令牌。
Marius Lazar

4
@JosephCarroll通常没有足够的特权是403
andrea.spot。

8

Andrei Ostrovski的最终解决方案确实非常有效,但如果刷新令牌也过期(假设您要进行刷新的api调用),该解决方案将无法正常工作。经过一番挖掘,我意识到刷新令牌API调用也被拦截器拦截。我必须添加一个if语句来处理此问题。

 intercept( request: HttpRequest<any>, next: HttpHandler ):Observable<any> {
   this.authService = this.injector.get( AuthenticationService );
   request = this.addAuthHeader(request);

   return next.handle( request ).catch( error => {
     if ( error.status === 401 ) {

     // The refreshToken api failure is also caught so we need to handle it here
       if (error.url === environment.api_url + '/refresh') {
         this.refreshTokenHasFailed = true;
         this.authService.logout();
         return Observable.throw( error );
       }

       return this.refreshAccessToken()
         .switchMap( () => {
           request = this.addAuthHeader( request );
           return next.handle( request );
         })
         .catch((err) => {
           this.refreshTokenHasFailed = true;
           this.authService.logout();
           return Observable.throw( err );
         });
     }

     return Observable.throw( error );
   });
 }

您能证明您和refreshTokenHasFailedboolean成员一起玩的其他地方吗?
Stephane '18

1
您可以在上面的Andrei Ostrovski的解决方案上找到它,我基本上使用了它,但是添加了if语句来处理刷新端点被拦截时的情况。
詹姆斯·里乌

这没有意义,为什么刷新会返回401?问题的关键是,它调用了刷新后验证失败,那么你的刷新API不应该在所有的认证,并且不应该返回一个401
MDave

刷新令牌可以有有效期。在我们的用例中,它设置为4小时后过期,如果用户要在一天结束时关闭其浏览器并在第二天早上返回,则刷新令牌将在该点之前过期,因此我们要求他们登录再回来。如果您的刷新令牌没有过期,那么您当然不需要应用此逻辑
James Lieu

4

根据这个例子,这是我的作品

@Injectable({
    providedIn: 'root'
})
export class AuthInterceptor implements HttpInterceptor {

    constructor(private loginService: LoginService) { }

    /**
     * Intercept request to authorize request with oauth service.
     * @param req original request
     * @param next next
     */
    intercept(req: HttpRequest<any>, next: HttpHandler): Observable<any> {
        const self = this;

        if (self.checkUrl(req)) {
            // Authorization handler observable
            const authHandle = defer(() => {
                // Add authorization to request
                const authorizedReq = req.clone({
                    headers: req.headers.set('Authorization', self.loginService.getAccessToken()
                });
                // Execute
                return next.handle(authorizedReq);
            });

            return authHandle.pipe(
                catchError((requestError, retryRequest) => {
                    if (requestError instanceof HttpErrorResponse && requestError.status === 401) {
                        if (self.loginService.isRememberMe()) {
                            // Authrozation failed, retry if user have `refresh_token` (remember me).
                            return from(self.loginService.refreshToken()).pipe(
                                catchError((refreshTokenError) => {
                                    // Refresh token failed, logout
                                    self.loginService.invalidateSession();
                                    // Emit UserSessionExpiredError
                                    return throwError(new UserSessionExpiredError('refresh_token failed'));
                                }),
                                mergeMap(() => retryRequest)
                            );
                        } else {
                            // Access token failed, logout
                            self.loginService.invalidateSession();
                            // Emit UserSessionExpiredError
                            return throwError(new UserSessionExpiredError('refresh_token failed')); 
                        }
                    } else {
                        // Re-throw response error
                        return throwError(requestError);
                    }
                })
            );
        } else {
            return next.handle(req);
        }
    }

    /**
     * Check if request is required authentication.
     * @param req request
     */
    private checkUrl(req: HttpRequest<any>) {
        // Your logic to check if the request need authorization.
        return true;
    }
}

您可能需要检查是否允许用户Remember Me使用刷新令牌重试或仅重定向到注销页面。

仅供参考,LoginService具有以下方法: -getAccessToken
():字符串-返回当前access_token
值-isRememberMe():布尔值-检查用户是否具有refresh_token
-refreshToken():可观察/承诺-向oauth服务器请求新access_token使用-invalidateSession refresh_token
():无效-删除所有用户信息并重定向到注销页面


您是否有多个请求发送多个刷新请求的问题?
CodingGorilla

我最喜欢这个版本,但是我遇到了一个问题,我的发出请求,当返回401时尝试刷新,当返回错误时,它连续尝试再次发送请求,永不停止。我在做错什么吗?
jamesmpw

抱歉,之前我没有仔细测试。刚刚用我正在使用的经过测试的帖子编辑了我的帖子(也可以迁移到rxjs6并刷新令牌,检查url)。
Thanh Nhan

1

理想情况下,您要isTokenExpired在发送请求之前进行检查。如果过期,则刷新令牌,并在标题中添加刷新的内容。

除此之外,这retry operator可能有助于您刷新401响应上的令牌的逻辑。

RxJS retry operator在您提出要求的服务中使用。它接受一个retryCount参数。如果未提供,它将无限期地重试该序列。

在响应的拦截器中刷新令牌并返回错误。当您的服务恢复错误但现在使用重试操作符时,它将重试请求,这次是使用刷新的令牌(Interceptor使用刷新的令牌添加标头)。

import {HttpClient} from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Rx';

@Injectable()
export class YourService {

  constructor(private http: HttpClient) {}

  search(params: any) {
    let tryCount = 0;
    return this.http.post('https://abcdYourApiUrl.com/search', params)
      .retry(2);
  }
}

0
To support ES6 syntax the solution needs to be bit modify and that is as following also included te loader handler on multiple request


        private refreshTokenInProgress = false;
        private activeRequests = 0;
        private tokenRefreshedSource = new Subject();
        private tokenRefreshed$ = this.tokenRefreshedSource.asObservable();
        private subscribedObservable$: Subscription = new Subscription();



 intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        if (this.activeRequests === 0) {
            this.loaderService.loadLoader.next(true);
        }
        this.activeRequests++;

        // Handle request
        request = this.addAuthHeader(request);

        // NOTE: if the flag is true it will execute retry auth token mechanism ie. by using refresh token it will fetch new auth token and will retry failed api with new token
        if (environment.retryAuthTokenMechanism) {
            // Handle response
            return next.handle(request).pipe(
                catchError(error => {
                    if (this.authenticationService.refreshShouldHappen(error)) {
                        return this.refreshToken().pipe(
                            switchMap(() => {
                                request = this.addAuthHeader(request);
                                return next.handle(request);
                            }),
                            catchError(() => {
                                this.authenticationService.setInterruptedUrl(this.router.url);
                                this.logout();
                                return EMPTY;
                            })
                        );
                    }

                    return EMPTY;
                }),
                finalize(() => {
                    this.hideLoader();
                })
            );
        } else {
            return next.handle(request).pipe(
                catchError(() => {
                    this.logout();
                    return EMPTY;
                }),
                finalize(() => {
                    this.hideLoader();
                })
            );
        }
    }

    ngOnDestroy(): void {
        this.subscribedObservable$.unsubscribe();
    }

    /**
     * @description Hides loader when all request gets complete
     */
    private hideLoader() {
        this.activeRequests--;
        if (this.activeRequests === 0) {
            this.loaderService.loadLoader.next(false);
        }
    }

    /**
     * @description set new auth token by existing refresh token
     */
    private refreshToken() {
        if (this.refreshTokenInProgress) {
            return new Observable(observer => {
                this.subscribedObservable$.add(
                    this.tokenRefreshed$.subscribe(() => {
                        observer.next();
                        observer.complete();
                    })
                );
            });
        } else {
            this.refreshTokenInProgress = true;

            return this.authenticationService.getNewAccessTokenByRefreshToken().pipe(tap(newAuthToken => {
            this.authenticationService.updateAccessToken(newAuthToken.access_token);
            this.refreshTokenInProgress = false;
            this.tokenRefreshedSource.next();
        }));
        }
    }

    private addAuthHeader(request: HttpRequest<any>) {
        const accessToken = this.authenticationService.getAccessTokenOnly();
        return request.clone({
            setHeaders: {
                Authorization: `Bearer ${accessToken}`
            }
        });
    }

    /**
     * @todo move in common service or auth service once tested
     * logout and redirect to login
     */
    private logout() {
        this.authenticationService.removeSavedUserDetailsAndLogout();
    }

0

我必须解决以下要求:

  • for多次刷新令牌
  • refresh如果refreshToken失败,请注销用户
  • if如果用户在第一次刷新后遇到错误,请注销
  • token在刷新令牌时将所有请求排队

结果,我收集了不同的选项以刷新Angular中的令牌:

  • 使用tokenRefreshed$BehaviorSubject作为信号量的暴力破解
  • 在RxJS运算符中使用caught参数catchError重试请求失败的请求
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    let retries = 0;
    return this.authService.token$.pipe(
      map(token => req.clone({ setHeaders: { Authorization: `Bearer ${token}` } })),
      concatMap(authReq => next.handle(authReq)),
      // Catch the 401 and handle it by refreshing the token and restarting the chain
      // (where a new subscription to this.auth.token will get the latest token).
      catchError((err, restart) => {
        // If the request is unauthorized, try refreshing the token before restarting.
        if (err.status === 401 && retries === 0) {
          retries++;
    
          return concat(this.authService.refreshToken$, restart);
        }
    
        if (retries > 0) {
          this.authService.logout();
        }
    
        return throwError(err);
      })
    );
}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    return this.authService.token$.pipe(
      map(token => req.clone({ setHeaders: { Authorization: `Bearer ${token}` } })),
      concatMap(authReq => next.handle(authReq)),
      retryWhen((errors: Observable<any>) => errors.pipe(
        mergeMap((error, index) => {
          // any other error than 401 with {error: 'invalid_grant'} should be ignored by this retryWhen
          if (error.status !== 401) {
            return throwError(error);
          }
    
          if (index === 0) {
            // first time execute refresh token logic...
            return this.authService.refreshToken$;
          }
    
          this.authService.logout();
          return throwError(error);
        }),
        take(2)
        // first request should refresh token and retry,
        // if there's still an error the second time is the last time and should navigate to login
      )),
    );
}

所有这些选项都经过严格测试,可以在angular-refresh-token github repo中找到


-3

我得到了这个基于失败请求的URL创建新请求并发送失败请求的相同正文的信息。

 retryFailedRequests() {

this.auth.cachedRequests.forEach(request => {

  // get failed request body
  var payload = (request as any).payload;

  if (request.method == "POST") {
    this.service.post(request.url, payload).subscribe(
      then => {
        // request ok
      },
      error => {
        // error
      });

  }
  else if (request.method == "PUT") {

    this.service.put(request.url, payload).subscribe(
      then => {
       // request ok
      },
      error => {
        // error
      });
  }

  else if (request.method == "DELETE")

    this.service.delete(request.url, payload).subscribe(
      then => {
        // request ok
      },
      error => {
        // error
      });
});

this.auth.clearFailedRequests();        

}


-4

在您的authentication.service.ts中,应该注入一个HttpClient作为依赖项

constructor(private http: HttpClient) { }

然后,您可以按以下方式重新提交请求(在retryFailedRequests内部):

this.http.request(request).subscribe((response) => {
    // You need to subscribe to observer in order to "retry" your request
});

这是我最初的想法,但是http.request返回HttpEvent
Antoniossss
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.