刷新令牌OAuth身份验证Angular 4+

时间:2017-12-13 15:51:03

标签: angular http interceptor angular-http-interceptors

我正在使用Angular的Http分段,但我决定进行迁移并使用新的HttpClient,我正在尝试使用Interceptors来创建解决方案来管理当我需要刷新令牌以及何时需要修改标头以放置授权令牌时的情况。

2 个答案:

答案 0 :(得分:3)

首先我找到了这些帖子以及其他许多帖子:

...但如果您只想处理授权标题的操作,那么这些解决方案是完美的。然后我想出了这个解决方案

@Injectable()
export class RefreshTokenInterceptor implements HttpInterceptor {

  constructor(private injector: Injector, private authService: Auth) {
  }


  private getRequestWithAuthentication(request: HttpRequest<any>, next: HttpHandler, auth: OAuthService): Observable<HttpEvent<any>> {
    const  req = request.clone({
        headers: request.headers.set('Authorization', auth.getHeaderAuthorization())
      });
    return next.handle(req);
  }

  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    // To avoid cyclic dependency
    const auth = this.injector.get(OAuthService);

    if (auth.hasAuthorization()) {
      return this.getRequestWithAuthentication(request, next, auth);
    } else if (auth.hasAuthorizationRefresh() && request.url !== AUTHORIZE_URL) {
      return auth.refreshToken().flatMap(
        (res: any) => {
          auth.saveTokens(res);
          return this.getRequestWithAuthentication(request, next, auth);
        }
      ).catch(() => {
        return next.handle(request);
      });
    } else if (request.url === AUTHORIZE_URL) {
      return next.handle(request);
    }

    return this.getRequestWithAuthentication(request, next, auth);
  }
}

这个主要想法很简单:

  • 首先,我正在注入一项服务,我拥有确定我是否拥有令牌刷新令牌的所有逻辑,当然还有以下操作:保存并获得它。
  • 如果我有授权​​(将令牌放入标题中),我只是使用授权标题返回请求,如果不是,我会检查是否有刷新令牌,我试图从服务器获取它,然后我等待,直到我有令牌传递请求。
  • 常量 AUTHORIZE_URL 它是一个字符串,其中包含来自服务器的路径,用于获取令牌或刷新令牌。我之所以检查这个原因,是因为我在HttpClient中向OAuthService发出了请求,因此它也会从拦截器传来而且它会让如果我不检查就无限循环。
  

此解决方案在某些情况下运行正常,但事情是,例如令牌已过期并且您有多个请求,每个请求都会尝试刷新令牌。

在此之后我发现了这个解决方案,但我想知道你对我所做的代码和方式的看法。

好的,首先我创建了一个服务来保存刷新令牌请求的状态和Observable来知道请求何时完成。

这是我的服务:

@Injectable()
export class RefreshTokenService {
  public processing: boolean = false;
  public storage: Subject<any> = new Subject<any>();

  public publish(value: any) {
    this.storage.next(value);
  }
}
  

我注意到,如果我有两个Interceptor来刷新令牌并处理它并且一个用于放置授权头(如果存在),那就更好了。

这是用于刷新令牌的拦截器:

@Injectable()
  export class RefreshTokenInterceptor implements HttpInterceptor {

    constructor(private injector: Injector, private tokenService: RefreshTokenService) {
    }

    intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
      const auth = this.injector.get(OAuthService);
      if (!auth.hasAuthorization() && auth.hasAuthorizationRefresh() && !this.tokenService.processing && request.url !== AUTHORIZE_URL) {
        this.tokenService.processing = true;
        return auth.refreshToken().flatMap(
          (res: any) => {
            auth.saveTokens(res);
            this.tokenService.publish(res);
            this.tokenService.processing = false;
            return next.handle(request);
          }
        ).catch(() => {
          this.tokenService.publish({});
          this.tokenService.processing = false;
          return next.handle(request);
        });
      } else if (request.url === AUTHORIZE_URL) {
        return next.handle(request);
      }

      if (this.tokenService.processing) {
        return this.tokenService.storage.flatMap(
          () => {
            return next.handle(request);
          }
        );
      } else {
        return next.handle(request);
      }
    }
  }

所以我在这里等待刷新令牌可用或失败,然后我发布需要授权标题的请求。

这是放置授权标头的拦截器:

@Injectable()
  export class TokenInterceptor implements HttpInterceptor {
    constructor(private injector: Injector) {}

    intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
      const auth = this.injector.get(OAuthService);
      let req = request;
      if (auth.hasAuthorization()) {
        req = request.clone({
          headers: request.headers.set('Authorization', auth.getHeaderAuthorization())
        });
      }

      return next.handle(req).do(
        () => {},
        (error: any) => {
          if (error instanceof HttpErrorResponse) {
            if (error.status === 401) {
              auth.logOut();
            }
          }
        });
    }
  }

我的主要模块是这样的:

@NgModule({
  imports: [
    ...,
    HttpClientModule
  ],
  declarations: [
    ...
  ],
  providers: [
    ...
    OAuthService,
    AuthService,
    RefreshTokenService,
    {
      provide: HTTP_INTERCEPTORS,
      useClass: RefreshTokenInterceptor,
      multi: true
    },
    {
      provide: HTTP_INTERCEPTORS,
      useClass: TokenInterceptor,
      multi: true
    }
  ],
  bootstrap: [AppComponent]
})
export class AppModule {
}

请欢迎任何反馈,如果我发错了,请告诉我。我使用 Angular 4.4.6进行测试,但我不知道它是否适用于角度5,我认为应该可行。

答案 1 :(得分:1)

对于在Angular 4中寻找解决方案的任何人(可能是Angular 5+需要稍作修改),我想出了以下解决方案:

@Injectable()
export class AuthInterceptorService implements HttpInterceptor {
    private _refreshRequest: Observable<ApiResult<any>> | null = null;

    constructor(
        private _router: Router, 
        private _tokenStorage: TokenStorageService,
        private _injector: Injector) {
    }

    private _addTokenHeader(request: HttpRequest<any>) {
        const authToken = this._tokenStorage.authToken;

        if (!authToken) {
            return request;
        }

        return request.clone({setHeaders: {'Authorization': 'Bearer ' + authToken.value}});
    }

    private _fail() {
        this._tokenStorage.clearTokens();
        this._router.navigate(['/login']);
        return throwError(new HttpErrorResponse({status: 401}));
    }

    private _refreshAuthToken(request: HttpRequest<any>, next: HttpHandler) {
        // AuthService has the following dependency chain:
        // ApiService -> HttpClient -> HTTP_INTERCEPTORS
        // If injected at the constructor this causes a circular dependency error. 
        const authService = <AuthService>this._injector.get(AuthService);

        if (this._refreshRequest === null) {
            // Send the auth token refresh request
            this._refreshRequest = authService.refreshAuthToken();
            this._refreshRequest.subscribe(() => this._refreshRequest = null);
        }

        // Wait for the auth token refresh request to finish before sending the pending request
        return this._refreshRequest
            .flatMap(result => {
                if (result.success) {
                    // Auth token was refreshed, continue with pending request
                    return this._sendRequest(this._addTokenHeader(request), next);
                }

                // Refreshing the auth token failed, fail the pending request
                return this._fail();
            });
    }

    private _sendRequest(request: HttpRequest<any>, next: HttpHandler) {
        return next.handle(request).catch((err: HttpErrorResponse, caught) => {
            // Send the user to the login page If there are any 'Unauthorized' responses
            if (err.status === 401) {
                this._router.navigate(['/login']);
            }

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

    intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        if (request.url.indexOf('/api/auth') !== -1) {
            // This interceptor should not be applied to auth requests
            return this._sendRequest(request, next);
        }

        const authToken = this._tokenStorage.authToken;
        const refreshToken = this._tokenStorage.refreshToken;

        // Attempt to refresh the auth token if it is expired or about to expire
        if (authToken && authToken.expiresWithinSeconds(60)) {
            if (refreshToken && !refreshToken.isExpired) {
                return this._refreshAuthToken(request, next);
            }
            else {
                // Auth token has expired and cannot be refreshed
                return this._fail();
            }
        }

        return this._sendRequest(this._addTokenHeader(request), next);
    }
}

如果当前的身份验证令牌已过期,这将向服务器发出身份验证令牌刷新请求,但是有一个有效的刷新令牌。进一步的请求被缓冲,直到挂起的刷新请求完成。

未显示的来源是:
- TokenStorageService只使用localStorage
- Jwt类包装令牌并使令牌声明像到期日期易于访问
- ApiResult这只是我的应用程序HttpResponse的简单包装器,与此处的任何内容都没有特别相关

编辑: Angular 6/7

import { Injectable, Inject, Injector } from '@angular/core';
import { Router } from '@angular/router';
import { 
    HttpEvent, 
    HttpInterceptor, 
    HttpHandler, 
    HttpRequest, 
    HttpErrorResponse, 
} from '@angular/common/http';
import { Observable, of, throwError } from 'rxjs';
import { catchError, flatMap } from 'rxjs/operators';

import { ApiResult } from '../../api';

import { TokenStorageService } from './token-storage.service';
import { AuthService } from './auth.service';


@Injectable()
export class AuthInterceptorService implements HttpInterceptor {
    private _refreshRequest: Observable<ApiResult> | null = null;

    constructor(
        private _router: Router, 
        private _tokenStorage: TokenStorageService,
        @Inject('BASE_URL') private _baseUrl: string,
        private _injector: Injector) {
    }

    private _addTokenHeader(request: HttpRequest<any>) {
        const authToken = this._tokenStorage.authToken;

        if (!authToken) {
            return request;
        }

        return request.clone({setHeaders: {'Authorization': 'Bearer ' + authToken.value}});
    }

    private _forceLogin() {
        this._tokenStorage.clearTokens();

        this._router.navigate(['/account/login'], { queryParams: {
            message: 'Your session has expired. Please re-enter your credentials.'
        }});
    }

    private _fail() {
        this._forceLogin();
        return throwError(new HttpErrorResponse({status: 401}));
    }

    private _refreshAuthToken(request: HttpRequest<any>, next: HttpHandler) {
        // AuthService has the following dependency chain:
        // ApiService -> HttpClient -> HTTP_INTERCEPTORS
        // If injected at the constructor this causes a circular dependency error. 
        const authService = <AuthService>this._injector.get(AuthService);

        if (this._refreshRequest === null) {
            // Send the auth token refresh request
            this._refreshRequest = authService.refreshAuthToken();
            this._refreshRequest.subscribe(() => this._refreshRequest = null);
        }

        // Wait for the auth token refresh request to finish before sending the pending request
        return this._refreshRequest.pipe(flatMap(result => {
            if (result.success) {
                // Auth token was refreshed, continue with pending request
                return this._sendRequest(this._addTokenHeader(request), next);
            }

            // Refreshing the auth token failed, fail the pending request
            return this._fail();
        }));
    }

    private _sendRequest(request: HttpRequest<any>, next: HttpHandler) {
        return next.handle(request).pipe(catchError(err => {
            // Send the user to the login page If there are any 'Unauthorized' responses
            if (err.status === 401) {
                this._forceLogin();
            }

            return throwError(err);
        }));
    }

    intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        if (request.url.indexOf(this._baseUrl) === -1  || request.url.indexOf('/api/auth') !== -1) {
            // This interceptor should not be applied to non-api requests or auth requests
            return this._sendRequest(request, next);
        }

        const authToken = this._tokenStorage.authToken;
        const refreshToken = this._tokenStorage.refreshToken;

        // Attempt to refresh the auth token if it is expired or about to expire
        if (authToken && authToken.expiresWithinSeconds(60)) {
            if (refreshToken && !refreshToken.isExpired) {
                return this._refreshAuthToken(request, next);
            }
            else {
                // Auth token has expired and cannot be refreshed
                return this._fail();
            }
        }

        return this._sendRequest(this._addTokenHeader(request), next);
    }
}