//core.module.ts //imports.... @NgModule({ providers: [ AuthService, { provide: HTTP_INTERCEPTORS, // interceptor` auth header useClass: ApplyTokenInterceptor, multi: true }, { provide: HTTP_INTERCEPTORS, // useClass: RefreshTokenInterceptor, multi: true } ], exports: [HttpClientModule] }) export class CoreModule { //@Optional() @SkipSelf() - CoreModule AppModule UserModule - constructor(@Optional() @SkipSelf() parentModule: CoreModule, userService: UserService, inj: Injector, auth: AuthService, http: HttpClient) { // AuthInterceptor let interceptors = inj.get<AuthInterceptor[]>(HTTP_INTERCEPTORS) .filter(i => { return i.init; }); // http . interceptors.forEach(i => i.init(http, auth)); userService.init(); if (parentModule) { // , CoreModule throw new Error( 'CoreModule is already loaded. Import it in the AppModule only'); } } }
export interface AuthInterceptor { init(http: HttpClient, auth: AuthRefreshProvider); }
export const HTTP_INTERCEPTORS = new InjectionToken<HttpInterceptor[]>('HTTP_INTERCEPTORS');
@Injectable() export class ApplyTokenInterceptor implements HttpInterceptor, AuthInterceptor { //circular dependency Error! public constructor(private http: HttpClient) { //.... } //.... }
@Injectable() export class ApplyTokenInterceptor implements HttpInterceptor, AuthInterceptor { public constructor(private injector: Injector) { //circular dependency Error! injector.get(HttpClient); } //.... }
//auth.service.ts export class AuthService implements AuthRefreshProvider { constructor(client: HttpClient) { } } //apply.interceptor.ts export class ApplyTokenInterceptor implements HttpInterceptor, AuthInterceptor { //circular dependency Error! , AuthService HttpClient public constructor(private auth: AuthService ) { } }
- Previously, an interceptor of the interceptor attempting to interceptor interceptor instances. Users want to inject HttpClient into interceptors to make supporting;
- Either HttpClient or Dependency. This change moves that responsibility into HttpClient itself. By utilizing a new class HttpInterceptingHandler, it’s possible that you’ve no longer need it.
@Injectable() export class ApplyTokenInterceptor implements HttpInterceptor, AuthInterceptor { private http: HttpClient; constructor(private injector: Injector) { } intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { // HttpClient if (!http) { this.http = injector.get(HttpClient); } } }
@NgModule({ providers: [ AuthService, UserService, export class CoreModule { { provide: APP_INITIALIZER, // : (a) => a.renewAuth() // ! renewAuth Promise, Observable . // // , AOT useFactory: refreshToken, deps: [AuthService], multi: true }, { provide: HTTP_INTERCEPTORS, useClass: ApplyTokenInterceptor, multi: true }, { provide: HTTP_INTERCEPTORS, useClass: RefreshTokenInterceptor, multi: true } ], exports: [HttpClientModule] }) export class CoreModule { // some code }
export function refreshToken(auth: AuthService) { return () => { // return own subject to complete this initialization step in any case // otherwise app will stay on preloader if any error while token refreshing occurred const subj = new Subject(); auth.renewAuth() .finally(() => { subj.complete(); }) .catch((err, caught: Observable<any>) => { // do logout, redirect to login will occurs at UserService with onLoggedOut event auth.logout(); return ErrorObservable.create(err); }) .subscribe(); // need to return Promise!! return subj.toPromise(); }; }
//auth.service.ts public renewAuth(): Observable < string > { if(!this.isNeedRefreshToken()) { return Observable.empty<string>(); } return this._http.post<string>(`https://${authConfig.domain}/oauth/token`, { client_id: authConfig.clientID, grant_type: 'refresh_token', refresh_token: localStorage.getItem('refresh_token') }).do(res => this.onAuthRenew(res)); } public isNeedRefreshToken(): boolean { //expires_at - , let expiresAtString = localStorage.getItem('expires_at'); if (!expiresAtString) { return false; } const expiresAt = JSON.parse(expiresAtString); //, , let isExpireInMinute = new Date().getTime() > (expiresAt - 60000); return isExpireInMinute; }
export class ApplyTokenInterceptor implements HttpInterceptor, AuthInterceptor { intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { //. HttpInterceptor , Authorization // API if (!req.url.includes('api/')) { return next.handle(req); } // , const authReq = req.clone({ headers: req.headers.set('Authorization', this.auth.authHeader) }); // return next.handle(authReq); } }
let first = service.getData(); // let second = service.getData(); let concatenated = first.concat(second); concatenated.subscribe(); // subscribe .
// 401 "" //"" - Observable type CallerRequest = { subscriber: Subscriber<any>; failedRequest: HttpRequest<any>; }; @Injectable() export class RefreshTokenInterceptor implements HttpInterceptor, AuthInterceptor { private auth: AuthRefreshProvider; private http: HttpClient; private refreshInProgress: boolean; private requests: CallerRequest[] = []; init(http: HttpClient, auth: AuthRefreshProvider) { /*some init;*/ } intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { // "" if(!req.url.includes('api/')) { return next.handle(req); } // Observable , Observable // Observable, let observable = new Observable<HttpEvent<any>>((subscriber) => { // HttpRequest // let originalRequestSubscription = next.handle(req) .subscribe((response) => { // (success) subscriber.next(response); }, (err) => { if (err.status === 401) { // 401 - this.handleUnauthorizedError(subscriber, req); } else { // subscriber.error(err); } }, () => { // , finally() subscriber.complete(); }); return () => { // // , dev tools , . ( Controller) , originalRequestSubscription.unsubscribe(); }; }); // Observable, . return observable; } //private handleUnauthorizedError //private repeatFailedRequests //private repeatRequest }
private handleUnauthorizedError(subscriber: Subscriber < any >, request: HttpRequest<any>) { // "401" this.requests.push({ subscriber, failedRequest: request }); if(!this.refreshInProgress) { // , , "401" // refresh this.refreshInProgress = true; this.auth.renewAuth() .finally(() => { this.refreshInProgress = false; }) .subscribe((authHeader) => // , this.repeatFailedRequests(authHeader), () => { // - , this.auth.logout(); }); } } private repeatFailedRequests(authHeader) { this.requests.forEach((c) => { // "" , const requestWithNewToken = c.failedRequest.clone({ headers: c.failedRequest.headers.set('Authorization', authHeader) }); // ( .subscriber - subscriber ) this.repeatRequest(requestWithNewToken, c.subscriber); }); this.requests = []; } private repeatRequest(requestWithNewToken: HttpRequest < any >, subscriber: Subscriber<any>) { // this.http.request(requestWithNewToken).subscribe((res) => { subscriber.next(res); }, (err) => { if (err.status === 401) { // if just refreshed, but for unknown reasons we got 401 again - logout user this.auth.logout(); } subscriber.error(err); }, () => { subscriber.complete(); }); }
Source: https://habr.com/ru/post/351036/
All Articles