简体   繁体   English

在拦截器中使用 observable

[英]Use an observable inside of an interceptor

I want to write an interceptor to add an auth token to all the requests.我想编写一个拦截器来为所有请求添加一个身份验证令牌。 My token comes from a lib, angularx-social-login, that only provides an Observable to get the token.我的令牌来自一个 lib,angularx-social-login,它只提供一个 Observable 来获取令牌。 So I wrote this but my request is never sent, as if the value was never outputed from the observable.所以我写了这个,但我的请求从未发送过,好像该值从未从 observable 中输出。

import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest} from "@angular/common/http";
import { SocialAuthService } from "angularx-social-login";
import {Observable, switchMap} from "rxjs";
import {Injectable} from "@angular/core";

@Injectable()
export class AuthInterceptorService implements HttpInterceptor {
  constructor(private authService: SocialAuthService) {}
  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    return this.authService.authState.pipe(
      switchMap((user) => {
        const token = user.idToken
        if (token) {
          request = request.clone({
            setHeaders: {Authorization: `Bearer ${token}`}
          });
        }
        return next.handle(request)
      })
    );
  }
}

I guess what happens here is that by the time the subscription to the observable occurs, the value of authState was already emitted in the login process, so the request hangs waiting for a new value to be emitted, which happened already in the login process.我猜这里发生的情况是,当订阅 observable 时,authState 的值已经在登录过程中发出,所以请求挂起等待发出新值,这已经在登录过程中发生。

In order to deal with this, I suggest that you implement a service (providedInRoot) to be injected into the Login component and retrieve the user data in the login process.为了解决这个问题,我建议你实现一个服务(providedInRoot)注入到登录组件中,并在登录过程中检索用户数据。

You could subscribe to the authState observable of SocialAuthService service in the OnInit of the Login component:您可以在 Login 组件的 OnInit 中订阅 SocialAuthService 服务的 authState observable:

import { Component, OnInit, OnDestroy } from '@angular/core';
import { SocialAuthService } from "angularx-social-login";
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';

// ... the rest of import

@Component({
  // ... Component decorator props (selector, templateUrl, styleUrls)
})
export class LoginComponent implements OnInit, OnDestroy {
  destroy = new Subject<boolean>();

  constructor(private authService: SocialAuthService, myUserService: MyUserService) { }

  ngOnInit(): void {
    this.authService.authState.pipe(takeUntil(this.destroy)).subscribe((user) => {
      this.myUserService.user = user;
    });
  }

  // ... the rest of the component

  ngOnDestroy(): void {
    this.destroy.next(true);
    this.destroy.complete();
  }

}

Then you could use the value from myUserService.user in your interceptor to retrieve the token.然后,您可以在拦截器中使用来自myUserService.user的值来检索令牌。

I have used the takeUntil rxjs operator with a Subject for ngOnDestroy, but you could also store the subscription as a class variable and perform the unsubscribe.我已将 takeUntil rxjs 运算符与 ngOnDestroy 的 Subject 一起使用,但您也可以将订阅存储为类变量并执行取消订阅。

import { Component, OnInit, OnDestroy } from '@angular/core';
import { SocialAuthService } from "angularx-social-login";
import { Subscription } from 'rxjs';

// ... the rest of import

@Component({
  // ... Component decorator props (selector, templateUrl, styleUrls)
})
export class LoginComponent implements OnInit, OnDestroy {
  authStateSubscription: Subscription;

  constructor(private authService: SocialAuthService, myUserService: MyUserService) { }

  ngOnInit(): void {
    this.authStateSubscription = this.authService.authState.subscribe((user) => {
      this.myUserService.user = user;
    });
  }

  // ... the rest of the component

  ngOnDestroy(): void {
    this.authStateSubscription.unsubscribe();
  }

}

In both ways should work.两种方式都应该有效。

I tend to agree with @cybering explanation, to add to his solution, you could define authState to be a BehaviorSubject.我倾向于同意@cybering 的解释,为了添加到他的解决方案中,您可以将authState定义为 BehaviorSubject。 this way no matter when some component subscribes to it, it'll get an emitted value as BehaviorSubject "saves" the last emitted value and emits it upon new subscriptions without being dependent on time.这样,无论某个组件何时订阅它,它都会获得一个发出的值,因为 BehaviorSubject “保存”最后一个发出的值,并在新订阅时发出它,而不依赖于时间。

I am not 100% sure on this, but I think using a switchMap you are switching to a new observable so return next.handle(request) never gets nexted.我对此不是 100% 确定的,但我认为使用switchMap你正在切换到一个新的 observable,所以return next.handle(request)永远不会被下一步。 Seems like authState is an observable, so we could do pipe(take(1)) without the need to unsubscribe.好像 authState 是一个可观察的,所以我们可以在不需要取消订阅的情况下执行pipe(take(1))

I would be more concise and split up getting the token and setting Bearer.我会更简洁,分开获取令牌和设置承载。

import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
import { SocialAuthService } from 'angularx-social-login';
import { Observable } from 'rxjs';
import { Injectable } from '@angular/core';

@Injectable()
export class AuthInterceptorService implements HttpInterceptor {
  constructor(private authService: SocialAuthService) {}
  intercept( request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {

    let token;
    this.authService.authState.pipe(take(1)).subscribe(data => token = data);

    if (token) {
      // If we have a token, we set it to the header
      request = request.clone({
        setHeaders: { Authorization: `Bearer ${token}` },
      });
    }

    return next.handle(request).pipe(
      catchError((err) => {
        if (err instanceof HttpErrorResponse) {
          if (err.status === 401) {
            // redirect user to the logout page
          }
        }
        return throwError(err);
      })
    );
  }
}

Would also recommend refactoring the way token can be received.还建议重构接收令牌的方式。 Like updating it to some storage and receiving it from storage via authService.就像将其更新到某个存储并通过 authService 从存储中接收它一样。 Subscribing each time to an observable is a pain.每次订阅 observable 都是一种痛苦。

AuthService.ts身份验证服务.ts

getAuthToken():string {
 return localeStarage.getItem('token')
 }

You could then just do:然后你可以这样做:

const token = this.authService.getAuthToken();

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM