2017-07-19 50 views
17

嗨我想弄清楚如何實現新的角度攔截器,並通過刷新令牌並重試請求來處理401 unauthorized錯誤。這是我一直遵循的指南:https://ryanchenkie.com/angular-authentication-using-the-http-client-and-http-interceptors在令牌刷新後的角度4攔截器重試請求

我成功緩存失敗的請求,並可以刷新令牌,但我無法弄清楚如何重新發送以前失敗的請求。我也想讓它與我目前使用的解析器一起工作。

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

      } 
     } 
    }); 

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

上述retryFailedRequests()文件是什麼,我想不通。如何重新發送請求並在重試後通過解析器使其可用於路由?

這是所有相關的代碼是否有幫助:https://gist.github.com/joshharms/00d8159900897dc5bed45757e30405f9

+1

我有同樣的問題,似乎沒有答案。 – LastTribunal

回答

6

我遇到了類似的問題還有,我想收集/重試邏輯,是過於複雜。相反,我們可以只使用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; 
} 
+0

我喜歡使用498的自定義狀態代碼來識別過期的令牌與401,這也可以表明不夠特權 –

+1

嗨,我試圖使用返回next.handle(reqClode)並且什麼都不做,我的代碼與你的升技不同,但是,不工作的部分是返回部分。 authService.createToken(authToken,refreshToken); this.inflightAuthRequest = null; return next.handle(req.clone({headers:req.headers.set(appGlobals.AUTH_TOKEN_KEY,authToken)})); –

+3

收集/重試邏輯不會過於複雜,如果您不希望在令牌過期時向refreshToken端點發出多個請求,則必須這樣做。說你的令牌過期了,你幾乎同時發出5個請求。通過本評論中的邏輯,服務器端會生成5個新的刷新令牌。 –

-1

在你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 
}); 
20

我的最終解決方案。與並行請求一起工作。

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

}

+0

在我的情況下,我有一個奇怪的錯誤,我需要調用'ApplicationRef.tick()',因爲我的註銷重定向到'/ login'頁面,在那裏有一個奇怪的直到檢測到更改發生時爲止,可以通過單擊調用該方法的鼠標。 – Lansana

+0

@Andrei Ostrovski我正在嘗試使用相同的解決方案,但'.flatMap'永遠不會被觸發。我嘗試用訂閱替換,並且工作正常,但隨後打破了可觀察的流,因爲我需要返回一個可觀察的。我該如何處理? – jerry

+1

@jerry檢查您是否使用CATCH處理程序,而不是DO –

2

基於this example,這裏是我的一塊

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

    // Authorize request 
    const authorizeRequest: Observable<HttpRequest<any>> = Observable.create((observer: Observer<HttpRequest<any>>) => { 
     // Retrieve access token from oauth service 
     const accessToken = this.oauthService.getAccessToken(); 
     // Clone the request to add the new header. (Request is immutable) 
     const authorizedReq = req.clone({ headers: req.headers.set('Authorization', 'Bearer ' + accessToken) }); 
     // Yield authorized request and complete 
     observer.next(authorizedReq); 
     observer.complete(); 
    }); 

    // Refresh token 
    const refreshToken = Observable.fromPromise(self.oauthService.refreshToken()).catch((refreshErr) => { 
     self.oauthService.logOut(); 
     self.router.navigate(['/pages/login']); 
     return Observable.empty(); 
    }).ignoreElements().shareReplay(); 

    // First, authorize request 
    return authorizeRequest 
     // Second, try proceed with request by `next.handle()` 
     .concatMap(authReq => next.handle(authReq)) 
     // Third, catch 401 Access token expired error then retry after refresh token 
     .catch((err, retry) => { 
      if (err instanceof HttpErrorResponse && err.status === 401) { 
       return Observable.concat(refreshToken, retry); 
      } 
      return Observable.throw(err); 
     }); 
} 

您可能需要檢查用戶啓用Remember Me使用刷新令牌重試或只是重定向到註銷頁。

+0

您是否有多個請求發送多個刷新請求的問題? – CodingGorilla

+0

這個版本我最喜歡,但我有一個問題,我的請求,當返回401是試圖刷新,當它返回錯誤它不斷嘗試ti再發送請求,永不停止。我做錯了什麼嗎? – jamesmpw

-1

我得到這個基於失敗的請求的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();   

}

0

理想情況下,你要發送的請求前檢查isTokenExpired。如果過期刷新令牌並添加刷新在標題。

除此之外retry operator可能會幫助您在401響應中刷新令牌的邏輯。

在您提出請求的服務中使用RxJS retry operator。它接受一個retryCount參數。 如果未提供,它將無限期地重試序列。

在您的攔截器響應中刷新令牌並返回錯誤。當你的服務回來的錯誤,但現在重試運營商正在使用,因此將重試請求,這次刷新令牌(攔截器使用刷新標記在頭部添加。)

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

安德烈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); 
    }); 
}