令牌刷新后的Angular 4 Interceptor重试请求

Kov*_*aci 67 javascript rxjs angular

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

我成功缓存了失败的请求并可以刷新令牌,但我无法弄清楚如何重新发送以前失败的请求.我也想让这个与我目前使用的解析器一起工作.

token.interceptor.ts

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();
                    }
                } );

            }
        }
    } );
Run Code Online (Sandbox Code Playgroud)

authentication.service.ts

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
    } );
}
Run Code Online (Sandbox Code Playgroud)

上面的retryFailedRequests()文件是我无法弄清楚的.如何重新发送请求并在重试后通过解析程序将其提供给路由?

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

And*_*ski 72

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

export class AuthInterceptor implements HttpInterceptor {

    authService;
    refreshTokenInProgress = false;

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

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

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

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

            return this.authService.refreshToken()
               .do(() => {
                    this.refreshTokenInProgress = false;
                    this.tokenRefreshedSource.next();
                });
        }
    }

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

    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).catch(error => {

            if (error.status === 401) {
                return this.refreshToken()
                    .switchMap(() => {
                        request = this.addAuthHeader(request);
                        return next.handle(request);
                    })
                    .catch(() => {
                        this.logout();
                        return Observable.empty();
                    });
            }

            return Observable.throw(error);
        });
    }
}
Run Code Online (Sandbox Code Playgroud)

  • @AndreiOstrovski,您能否用`imports` 和AuthService 的代码更新答案? (3认同)
  • 我有一种感觉,如果由于某种原因this.authService.refreshToken()失败,所有等待刷新的并行查询将永远等待. (3认同)
  • 刷新令牌上的捕获从不需要我.它击中了Observable .throw. (2认同)
  • 伙计们,它适用于并行和顺序请求。你发送5个请求,它们返回401,然后执行1个refreshToken,然后再次发送5个请求。如果您的 5 个请求是连续的,则在第一个 401 请求之后,我们发送刷新令牌,然后再次发送第一个请求和其他 4 个请求。 (2认同)
  • 如果你用 @Injectable() 装饰它,为什么 Angular 可以为你手动注入服务呢?另外一个 catchError 不会返回任何内容。至少返回“EMPTY”。 (2认同)
  • 假设两个请求失败。一个调用refreshtoken,另一个看到刷新正在进行,并在这里等待tokenRefreshed$.subscribe。如果第一个刷新请求失败,第二个请求不是会永远订阅 this.tokenRefreshed$ ,导致内存泄漏吗? (2认同)

Sam*_*pan 13

使用最新版本的Angular(7.0.0)和rxjs(6.3.3),这就是我创建一个功能齐全的自动会话恢复拦截器的方法,确保如果并发请求失败,那么它应该只会命中令牌刷新API一次并使用switchMap和Subject将失败的请求传递给该响应.下面是我的拦截器代码的样子.我已经省略了我的身份验证服务和存储服务的代码,因为它们是非常标准的服务类.

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;
  }
}
Run Code Online (Sandbox Code Playgroud)


小智 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;
Run Code Online (Sandbox Code Playgroud)

...

private isAuthError(error: any): boolean {
  return error instanceof HttpErrorResponse && error.status === 401;
}
Run Code Online (Sandbox Code Playgroud)

  • 收集/重试逻辑并不过分复杂,如果您不希望在令牌过期时向refreshToken端点发出多个请求,则必须采用这种方式.假设您的令牌已过期,并且几乎在同一时间发出5个请求.使用此注释中的逻辑,将在服务器端生成5个新的刷新令牌. (6认同)
  • @JosephCarroll通常没有足够的权限是403 (3认同)

Jam*_*ieu 7

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 );
   });
 }
Run Code Online (Sandbox Code Playgroud)

  • 刷新令牌可以有到期日期。在我们的用例中,它被设置为在 4 小时后过期,如果用户在一天结束时关闭浏览器并在第二天早上返回,刷新令牌到那时就会过期,因此我们要求他们登录又回来了。如果您的刷新令牌没有过期,那么您当然不需要应用此逻辑 (2认同)

yur*_*zui 7

我必须解决以下要求:

  • ? 多个请求只刷新一次令牌
  • ? 如果 refreshToken 失败,则注销用户
  • ? 如果用户在第一次刷新后出现错误,则注销
  • ? 在刷新令牌时将所有请求排队

因此,为了在 Angular 中刷新令牌,我收集了不同的选项:

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);
      })
    );
}
Run Code Online (Sandbox Code Playgroud)
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
      )),
    );
}
Run Code Online (Sandbox Code Playgroud)

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

也可以看看:


Tha*_*han 5

基于这个例子,这是我的作品

@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;
    }
}
Run Code Online (Sandbox Code Playgroud)

您可能想检查用户是否启用了Remember Me使用刷新令牌进行重试或仅重定向到注销页面。

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