Angular 4 和 OAuth - 拦截 401 响应,刷新访问令牌并重试请求

Posted

技术标签:

【中文标题】Angular 4 和 OAuth - 拦截 401 响应,刷新访问令牌并重试请求【英文标题】:Angular 4 and OAuth - Intercept 401 responses, refresh the access token and retry request 【发布时间】:2018-05-05 04:48:13 【问题描述】:

正如标题所说,我正在开发一个带有 OAuth 身份验证的 Angular 4 项目。

每当 http 请求响应状态码 401 时,我都会拦截该请求,更新访问令牌并重试失败的请求。

当我收到 401 时,请求被正确拦截,并且访问令牌会按应有的方式刷新。失败的请求也会再次执行,但不再向组件传递它的响应。

所以问题在于,我的组件应该在请求响应中观察到,在令牌刷新和请求重试发生之前,该组件会抱怨日志“接收属性时出错”。

我的拦截器:

import  Injectable, Inject, Injector  from '@angular/core';
import 
  HttpRequest,
  HttpHandler,
  HttpResponse,
  HttpErrorResponse,
  HttpEvent,
  HttpInterceptor,
  HttpSentEvent,
  HttpHeaderResponse,
  HttpProgressEvent,
  HttpUserEvent
 from '@angular/common/http';
import  Observable  from 'rxjs/Observable';
import  AuthService  from './auth.service';
import  BehaviorSubject  from 'rxjs/BehaviorSubject';
import  TokenManager  from '../../../util/TokenManager';
import  AuthUserResponse  from '../../../models/authUserResponse';
import 'rxjs/add/operator/switchMap';

@Injectable()
export class AuthTokenExpiredInterceptor implements HttpInterceptor 

  isRefreshingToken: boolean = false;
  tokenSubject: BehaviorSubject<string> = new BehaviorSubject<string>(null);

  constructor( private injector: Injector, private tokenManager: TokenManager ) 


  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpSentEvent | HttpHeaderResponse | HttpProgressEvent | HttpResponse<any> | HttpUserEvent<any>> 
    return next.handle(this.addNewAccessTokenToHeaders(request, this.tokenManager.retrieveAccessToken()))
    .do((event: HttpEvent<any>) => 
      if (event instanceof HttpResponse) 
          console.log('processing response', event);
      
      return event;
    ,(err) => 
      if (err instanceof HttpErrorResponse) 
        if (err.status === 401) 
          console.log('Access_token possibly expired, trying to retrieve a new one!')

          return this.handle401Error(request, next);
         else if (err.status === 400) 
          console.log('Refresh_token possibly expired, redirecting to login');

          return this.handle400Error(err);
        
       else 
        return Observable.throw(err);
      
    );
  

  handle401Error(request: HttpRequest<any>, next: HttpHandler) 
    if (!this.isRefreshingToken) 
      console.log('in if');
      this.isRefreshingToken = true;

      // Reset here so that the following requests wait until the token comes back from the refreshToken call.
      this.tokenSubject.next(null);

      console.log('About to call renewAccessToken');
      return this.injector.get(AuthService).renewAccessToken().subscribe((response) => 
        let newToken = response.access_token;

        if (newToken) 
          console.log('Got the new access_token!');
          this.tokenSubject.next(newToken);
          let requestToRetry = this.addNewAccessTokenToHeaders(request, newToken);
          console.log('The retried request header: ' + requestToRetry.headers.get("Authorization"));
          return next.handle(requestToRetry);
         else   // No token in response
          this.injector.get(AuthService).logout();
        
      ,
      (err) => 
        this.injector.get(AuthService).logout();
        return Observable.throw(err)
      ,
      () => 
        console.log('handle401Error done');
        this.isRefreshingToken = false;
      )        
     else 
      console.log('In else');
      return this.tokenSubject
      .filter(token => token != null)
      .take(1)
      .switchMap(token => 
        return next.handle(this.addNewAccessTokenToHeaders(request, token));
      );
    
  


    handle400Error(error: HttpErrorResponse) 
      if (error && error.status === 400 && error.error && error.error.error === 'invalid_grant') 
        this.injector.get(AuthService).logout();
      

        return Observable.throw(error);
      

    addNewAccessTokenToHeaders(req: HttpRequest<any>, token: string): HttpRequest<any> 
      console.log('Adding the access_token to the Authorization header');
      return req.clone( setHeaders: 
        Authorization: 'Bearer ' + token
      )
    
  

我的服务返回一个 Observable

和我的组件:

ngOnInit()
    this.getProperties();
  

  getProperties() 
    this.propertyService.getProperties().subscribe(
      result => 
      this.properties = result;
      console.log('Received response in Properties component: ' + JSON.stringify(result));
    , error => 
      console.log('Error receiving the properties for the view')
    ,
    () =>  console.log('Received the properties, now they can be displayed in the view') )
  

【问题讨论】:

【参考方案1】:

你的函数拦截必须总是返回一个 Observable > >。你的代码有点“怪诞”。我看到的主要问题是您使用“do”来捕获错误。 “不要”修改请求。

我有这样一个拦截(希望代码可以帮助到你)

constructor(private inj: Injector)  

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> 
    //if the request has "Authorization" we return the request
    if (req.headers.has('Authorization'))
      return next.handle(req);

    //I get here the AuthService
    const auth = this.inj.get(AuthService);

    //create the httpHeaders
    const httpHeaders = new HttpHeaders()
      .set('Content-Type', 'application/json; charset=utf-8')
      .set('Authorization', '' + auth.SID) //<-- I use auth.SID

    const authReq = req.clone( headers: httpHeaders );

    return next.handle(authReq).catch((err: any) =>  //<--if error use a catch
      if (err instanceof HttpErrorResponse) 
        if (err.status === 401) 
          //auth.recoverSID return a Observable<value:new SID>
          //use switchMap to really return next.handle(authReq)
          return auth.recoverSID().switchMap((value: IResponse) => 
            let httpHeaders = new HttpHeaders()
              .set('Content-Type', 'application/json; charset=utf-8')
              .set('Authorization', '' + value.SID)

            const authReq = req.clone( headers: httpHeaders );
            return next.handle(authReq);
          )
        ;
      
      //Other case throw an error
      return Observable.throw(err);
    );
  

【讨论】:

请注意 next.handle().catch 在 Angular 5 和最新的 RxJS 库中已更改:***.com/a/47841788/2988073 感谢广告,@closedloop(当我写答案时,Angular 仍然使用 Rxjs 5)现在我们必须使用 pipe(catchError(err=>...),参见 github.com/ReactiveX/rxjs/blob/master/docs_app/content/guide/v6/…

以上是关于Angular 4 和 OAuth - 拦截 401 响应,刷新访问令牌并重试请求的主要内容,如果未能解决你的问题,请参考以下文章

Angular 4/5 + Spring Boot + Oauth2 支持

如何在Angular 4中提供用户名和密码以使用具有oAuth安全性的rest控制器?

令牌刷新后Angular 4拦截器重试请求

Spring + Angular 2 + Oauth2 + CORS 的 CSRF 问题

如何在拦截器中取消当前请求 - Angular 4

预期发现文档中的颁发者无效:带有 Azure B2C 的 angular-oauth2-oidc