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

Refresh Token OAuth Authentication Angular 4+

我正在使用来自 Angular 的 Http 类,但我决定进行迁移并使用新的 HttpClient,并且我正在尝试使用 Interceptors 来管理我需要刷新令牌以及需要修改 header 以放置授权令牌的情况。

首先我找到了这些 post 和许多其他的 :

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

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

主要思想很简单:

  • 首先,我要注入一个服务,我有所有的逻辑来确定我是否有 tokenrefresh token当然还有保存和获取它的操作。
  • 如果我有授权​​(这是 token 放在 header 中)我只是 return 带有授权的请求 Header,如果没有,我会检查我是否有 刷新令牌 并且我正在尝试从服务器获取它,然后我等待直到我有令牌来传递请求.
  • 常量 AUTHORIZE_URL 它是一个字符串,其中包含来自我用来获取令牌或刷新令牌的服务器的路由。因为我正在检查这个的原因是因为我在 OAuthService 中使用 HttpClient 发出请求所以它也会从拦截器传递并且如果我不检查它会形成无限循环.

This Solution work fine in some cases, but the thing is when for example the token expired and you have multiple request, every request is going to try to refresh the token.


在此之后我找到了这个解决方案,但我想知道您对代码和我正在做的方式有何看法。

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

这是我的服务:

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

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

I noticed that It was better if I have two Interceptors one to refresh the token and handle that and one to put the Authorization Header if exist.

这是刷新令牌的拦截器:

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

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

这是放置授权的拦截器Header:

@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 进行测试,但我不知道它是否适用于 angular 5,我认为应该可以。

对于在 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 class 包装令牌并使令牌声明(如到期日期)易于访问
- 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);
    }
}