angular - rxJs & angular 4 & restangular : stack errorInterceptors

标签 angular rxjs observable restangular

在我的 Angular 4 应用程序中,我使用 ngx-restangular 来处理所有服务器调用。它返回 observable 作为结果,并且该模块具有处理错误(如 401 等)的钩子(Hook)。

但是根据文档,我可以处理 403 (401) 所以:

  RestangularProvider.addErrorInterceptor((response, subject, responseHandler) => {
    if (response.status === 403) {

      refreshAccesstoken()
      .switchMap(refreshAccesstokenResponse => {
        //If you want to change request or make with it some actions and give the request to the repeatRequest func.
        //Or you can live it empty and request will be the same.

        // update Authorization header
        response.request.headers.set('Authorization', 'Bearer ' + refreshAccesstokenResponse)

        return response.repeatRequest(response.request);
      })
      .subscribe(
        res => responseHandler(res),
        err => subject.error(err)
      );

      return false; // error handled
    }
    return true; // error not handled
  });

这对一个请求有好处,它因 403 错误而中断。 我如何使用 rxJs 堆叠此调用?因为现在,例如,我有 3 个请求,其中有 403 个,对于每个这个损坏的请求,我正在刷新 token - 这不是很好,我必须更新我的 token ,然后重复我所有损坏的请求。我如何使用 Observables 实现这一目标?

在 Angular 1 中非常简单:

Restangular.setErrorInterceptor(function (response, deferred, responseHandler) {
  if (response.status == 403) {
    // send only one request if multiple errors exist
    if (!refreshingIsInProgress) {
      refreshingIsInProgress = AppApi.refreshAccessToken(); // Returns promise
    }


    $q.when(refreshingIsInProgress, function () {
      refreshingIsInProgress = null;

      setHeaders(response.config.headers);

      // repeat request with error
      $http(response.config).then(responseHandler, deferred);
    }, function () {
      refreshingIsInProgress = null;

      $state.go('auth');
    });

    return false; // stop the promise chain
  }

  return true;
});

一切都很顺利。但我是 rxJs 和 angular 4 的新手,我不知道如何使用 observables 和 angular 4 来实现这一点。也许有人有想法?

更新! 这是我的 refreshAccesstoken 方法

const refreshAccesstoken = function () {
  const refreshToken = http.post(environment.apiURL + `/token/refresh`,
    {refreshToken: 'someToken'});
  return refreshToken;
};

最佳答案

我可以看到使用 ngx-restangular 执行此操作的一种方法是使用 share 运算符。这样您就不必实现复杂的排队逻辑。这个想法是,如果你有 3 个请求都带有 403 响应,它们都会命中你的拦截器并调用你的可观察对象。如果您共享该可观察对象,对于 3 个 token 损坏的请求,您将只有一个 token 请求。

您只需要像这样在代码中使用共享运算符:

refreshAccesstoken()
  .share()
  .switchMap(refreshAccesstokenResponse => {
    //If you want to change request or make with it some actions and give the request to the repeatRequest func.
    //Or you can live it empty and request will be the same.

    // update Authorization header
    response.request.headers.set('Authorization', 'Bearer ' + refreshAccesstokenResponse)

    return response.repeatRequest(response.request);
  })
  .subscribe(
    res => responseHandler(res),
    err => subject.error(err)
  );

我还没有检查过代码是否真的有效,但我之前在相同的用例中使用过这种方法,但我没有使用拦截器,而是使用了 angular HTTP 服务。

编辑以更改 refreshAccessToken:

您需要将 refreshAccessToken 方法包装在延迟的 Observable 中并共享它。这样您每次都会重用相同的可观察对象。

在构造函数中:

this.source = Observable.defer(() => {
        return this.refreshAccesstoken();
    }).share();

创建另一个将调用该可观察对象的方法:

refreshToken(): Observable<any> {
    return this.source
        .do((data) => {
            this.resolved(data);
        }, error => {
            this.resolved(error);
        });
}

EDIT2

我创建了一个 git repository它使用带有 restangular 的 angular2。 场景如下:

  1. 在我的 app.component 中,我发出了 3 个并发请求来获取订单列表。请求完成后,我将记录“Orders received”。
  2. orders 端点需要一个授权 token 。如果未提供,它将返回 401。
  3. 在我的 app.module 中,我仅将基本 URL 设置为我的 API。因为我也没有设置授权 token ,所以我的所有请求都将失败并显示 401。
  4. 执行拦截器代码时,它会设置刷新 token ,在我的例子中,它是硬编码在请求上并重复请求的。
  5. 返回 token 的可观察对象在每次执行时都会记录“获取 token ”。

这是我在控制台中看到的内容: console logs and requests

如果我删除共享运算符,我将获得以下日志: enter image description here 这意味着每次都会创建可观察对象。

为了让它工作,重要的是在 RestangularConfigFactory 中声明和创建源。它本质上将成为一个单例对象,这就是允许 Share 运算符工作的原因。

注意:

我为这个项目创建了一个简单的本地托管网络 API,因为它对我来说更快。

EDIT3:更新以包含刷新 token 的代码:

@Injectable()
export class TokenRefreshService {
    source: Observable<any>;
    pausedObservable: Observable<any>;
    constructor(
        private authenthicationStore: AuthenticationStore,
        private router: Router,
        private authenticationDataService: AuthenticationDataService,
        private http: ObservableHttpService) {
        this.source = Observable.defer(() => {
            return this.postRequest();
        }).share();
    }

    refreshToken(): Observable<any> {
        return this.source
            .do((data) => {
                this.resolved(data);
            }, error => {
                this.resolved(error);
            });
    }

    public shouldRefresh(): boolean {
        if (this.getTime() < 0) {
            return true;
        }
        return false;
    }

    private postRequest(): Observable<any> {
        let authData = this.authenticationDataService.getAuthenticationData();
        if (authData == null) {
            return Observable.empty();
        }
        let data: URLSearchParams = new URLSearchParams();
        data.append('grant_type', 'refresh_token');

        let obs = this.http.postWithHeaders(
            'token', data, { 'Content-Type': 'application/x-www-form-urlencoded' })
            .map((response) => {
                return this.parseResult(true, response, 'authenticateUserResult');
            })
            .catch((error) => {
                let errorMessage = this.rejected(error);
                return Observable.throw(errorMessage);
            });
        return obs;
    }

    private rejected(failure) {
        let authenticateUserResult;
        let response = failure;
        let data = response.json();

        if (response &&
            response.status === 400 &&
            data &&
            data.error &&
            data.error === 'invalid_grant') {

            authenticateUserResult = this.parseResult(false, data, 'error_description');

            return authenticateUserResult;
        } else {
            return failure;
        }
    }

    private parseResult(success, data, resultPropertyName) {

        let authenticateResultParts = data[resultPropertyName].split(':');

        data.result = {
            success: success,
            id: authenticateResultParts[0],
            serverDescription: authenticateResultParts[1]
        };

        return data;
    }

    private resolved(data): void {
        let authenticationResult = data.result;
        if (authenticationResult && authenticationResult.success) {
            let authenticationData = this.createAuthenticationData(data);
            this.authenthicationStore.setUserData(authenticationData);
        } else {
            this.authenthicationStore.clearAll();
            this.router.navigate(['/authenticate/login']);
        }
    }

    private createAuthenticationData(data: any): AuthenticationData {
        let authenticationData = new AuthenticationData();
        authenticationData.access_token = data.access_token;
        authenticationData.token_type = data.token_type;
        authenticationData.username = data.username;
        authenticationData.friendlyName = data.friendlyName;
       
        return authenticationData;
    }

    private getTime(): number {
        return this.getNumberOfSecondsBeforeTokenExpires(this.getTicksUntilExpiration());
    }

    private getTicksUntilExpiration(): number {
        let authData = this.authenticationDataService.getAuthenticationData();
        if (authData) {
            return authData.expire_time;
        }
        return 0;
    }

    private getNumberOfSecondsBeforeTokenExpires(ticksWhenTokenExpires: number): number {
        let a;
        if (ticksWhenTokenExpires === 0) {
            a = new Date(new Date().getTime() + 1 * 60000);
        } else {
            a = new Date((ticksWhenTokenExpires) * 1000);
        }

        let b = new Date();
        let utc1 = Date.UTC(a.getFullYear(), a.getMonth(), a.getDate(), a.getHours(), a.getMinutes(), a.getSeconds());
        let utc2 = Date.UTC(b.getFullYear(), b.getMonth(), b.getDate(), b.getHours(), b.getMinutes(), b.getSeconds());

        let timeInSeconds = Math.floor((utc1 - utc2) / 1000);
        return timeInSeconds - 5;
    }
}

关于angular - rxJs & angular 4 & restangular : stack errorInterceptors,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/44858086/

相关文章:

angular - 将按钮添加到 mat-tab-group

angular - 动态设置 [matMenuTriggerFor]

angular - 为什么不收集 FormControl#valueChanges 的订阅垃圾?

ionic-framework - Ionic 和 AngularFire2 取消订阅可观察对象的最佳实践

javascript - 将可观察量转换为数组

angular - CORE_DIRECTIVES 是否要添加到 Controller 的 'directives' 数组中以及如何正确使用它们?

node.js - Angular PWA - 当没有可用连接时路由到自定义离线页面

javascript - Angular 使用可观察的完整与下一个处理程序以及何时适本地使用每个处理程序

Angular 2 - typescript : TS2322: Type 'Subscription' is not assignable to type 'Observable<MouseEvent>'

system.reactive - 为什么 Rx Observable.Subscribe 会阻塞我的线程?