英文:
Why does the interceptor always return "null" (i.e., the initial value) using BehaviorSubject, not the updated value?
问题
我正在使用 PEAN 栈(即 PostgreSQL - ExpressJS - Angular - NodeJS)构建身份验证应用程序。
在后端,我检查用户登录状态如下:
- 在后端,检查会话 cookie 是否存在
req.session
对象的user
属性。
server.js
/* ... */
app.post('/api/get-signin-status', async (req, res) => {
try {
if (req.session.user) {
return res.status(200).json({ message: '用户已登录' });
} else {
return res.status(400).json({ message: '用户已注销' });
}
} catch {
return res.status(500).json({ message: '内部服务器错误' });
}
});
/* ... */
- 使用 HTTP POST 请求发送到
api/get-signin-status
端点,包括一个 cookie 在请求中。
auth.service.ts
/* ... */
getSignInStatus(data?: any) {
return this.http.post(this.authUrl + 'api/get-signin-status', data, {
withCredentials: true,
});
}
/* ... */
- 拦截任何 HTTP 请求并提供一个可观察的
interceptorResponse$
以订阅拦截请求的响应。
interceptor.service.ts
import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpEvent, HttpRequest, HttpHandler } from '@angular/common/http';
import { Observable, BehaviorSubject } from 'rxjs';
import { AuthService } from 'src/app/auth/services/auth.service';
@Injectable({
providedIn: 'root',
})
export class InterceptorService implements HttpInterceptor {
private interceptorResponse$: BehaviorSubject<any> = new BehaviorSubject<any>(null);
intercept(httpRequest: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const signInStatusObserver = {
next: (x: any) => {
this.interceptorResponse$.next({ success: true, response: x });
},
error: (err: any) => {
this.interceptorResponse$.next({ success: false, response: err });
},
};
this.authService.getSignInStatus().subscribe(signInStatusObserver);
return next.handle(httpRequest);
}
getInterceptorResponse(): Observable<any> {
return this.interceptorResponse$.asObservable();
}
constructor(private authService: AuthService) {}
}
- 在前端,从
InterceptorService
订阅interceptorResponse
可观察对象,并将响应记录到控制台。
header.component.ts
import { Component, OnInit } from '@angular/core';
import { InterceptorService } from '../auth/services/interceptor.service';
@Component({
selector: 'app-header',
templateUrl: './header.component.html',
styleUrls: ['./header.component.scss'],
})
export class HeaderComponent implements OnInit {
interceptorResponse: any;
constructor(private interceptorService: InterceptorService) {
this.interceptorService.getInterceptorResponse().subscribe((response: any) => {
console.log(response);
this.interceptorResponse = response;
if (response) {
console.log('拦截器响应成功:', response.response);
} else {
console.log('拦截器响应为空');
}
});
}
ngOnInit(): void {}
}
问题:
为什么 Angular 拦截器始终使用 BehaviorSubject
返回 null
(即初始值),而不是更新后的值?
编辑 1:
我注意到您在 app.module.ts
中配置了拦截器,请确保在 providers
数组中正确提供 InterceptorService
。
编辑 2:
根据您的编辑,您似乎已经解决了问题。您移除了不必要的代码,并对拦截器进行了必要的更改以匹配新的端点。现在,您可以根据后端传递的登录状态在前端显示不同的元素。
英文:
I'm building an authentication app using the PEAN stack (i.e., PostgreSQL - ExpressJS - Angular - NodeJS).
I check for user sign-in status as follows:
- On the backend, check the session cookie to see if the
user
property exists in thereq.session
object.
server.js
/* ... */
app.post('/api/get-signin-status', async (req, res) => {
try {
if (req.session.user) {
return res.status(200).json({ message: 'User logged in' });
} else {
return res.status(400).json({ message: 'User logged out' });
}
} catch {
return res.status(500).json({ message: 'Internal server error' });
}
});
/* ... */
- Send an HTTP POST request to the
api/get-signin-status
endpoint with optional data and include a cookie in the request.
auth.service.ts
/* ... */
getSignInStatus(data?: any) {
return this.http.post(this.authUrl + 'api/get-signin-status', data, {
withCredentials: true,
});
}
/* ... */
- Intercept any HTTP request and provide an observable
interceptorResponse$
for subscribing to the response of intercepted requests.
interceptor.service.ts
import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpEvent, HttpRequest, HttpHandler } from '@angular/common/http';
import { Observable, BehaviorSubject } from 'rxjs';
import { AuthService } from 'src/app/auth/services/auth.service';
@Injectable({
providedIn: 'root',
})
export class InterceptorService implements HttpInterceptor {
private interceptorResponse$: BehaviorSubject<any> = new BehaviorSubject<any>(null);
intercept(httpRequest: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const signInStatusObserver = {
next: (x: any) => {
this.interceptorResponse$.next({ success: true, response: x });
},
error: (err: any) => {
this.interceptorResponse$.next({ success: false, response: err });
},
};
this.authService.getSignInStatus().subscribe(signInStatusObserver);
return next.handle(httpRequest);
}
getInterceptorResponse(): Observable<any> {
return this.interceptorResponse$.asObservable();
}
constructor(private authService: AuthService) {}
}
- On the frontend, subscribe to the
interceptorResponse
observable from theInterceptorService
and log the response to the console.
header.component.ts
import { Component, OnInit } from '@angular/core';
import { InterceptorService } from '../auth/services/interceptor.service';
@Component({
selector: 'app-header',
templateUrl: './header.component.html',
styleUrls: ['./header.component.scss'],
})
export class HeaderComponent implements OnInit {
interceptorResponse: any;
constructor(
private interceptorService: InterceptorService
) {
this.interceptorService.getInterceptorResponse().subscribe((response: any) => {
console.log(response);
this.interceptorResponse = response;
if (response) {
console.log('Interceptor response success:', response.response);
} else {
console.log('Interceptor response is null');
}
});
}
ngOnInit(): void {}
}
Problem
According to the StackOverflow answer, I should use BehaviorSubject
. The problem is that in the console, I always get the following:
But if I console log next
and error
like this:
interceptor.service.ts
/* ... */
const signInStatusObserver = {
next: (x: any) => {
console.log(x);
this.interceptorResponse$.next({ success: true, response: x });
},
error: (err: any) => {
console.log(err.error.message);
this.interceptorResponse$.next({ success: false, response: err });
},
};
/* ... */
I see the expected {message: 'User logged in'}
in the console, as shown in the screenshot below. This means that the backend correctly passes sign-in status to the frontend.
Question
Why does the Angular interceptor always return null
(i.e., the initial value) using BehaviorSubject
, not the updated value?
EDIT 1
app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { HeaderComponent } from './header/header.component';
import { AppComponent } from './app.component';
import { FooterComponent } from './footer/footer.component';
import { AppRoutingModule } from './app-routing.module';
import { RoutingComponents } from './app-routing.module';
import { SharedModule } from './shared/shared.module';
import { HttpClientModule } from '@angular/common/http';
import { MatMenuModule } from '@angular/material/menu';
import { MatSidenavModule } from '@angular/material/sidenav';
import { CodeInputModule } from 'angular-code-input';
import { IfSignedOut } from './auth/guards/if-signed-out.guard';
import { IfSignedIn } from './auth/guards/if-signed-in.guard';
import { InterceptorService } from './auth/services/interceptor.service';
import { HTTP_INTERCEPTORS } from '@angular/common/http';
@NgModule({
declarations: [HeaderComponent, AppComponent, FooterComponent, RoutingComponents],
imports: [BrowserModule, BrowserAnimationsModule, AppRoutingModule, SharedModule, HttpClientModule, MatMenuModule, MatSidenavModule, CodeInputModule],
providers: [IfSignedOut, IfSignedIn, { provide: HTTP_INTERCEPTORS, useClass: InterceptorService, multi: true }],
bootstrap: [AppComponent],
})
export class AppModule {}
EDIT 2
With the help of @VonC I managed to get the whole thing working as expected. Here's what I did.
- I removed my initial code in
server.js
because the interceptor will now depend on theapi/get-user
endpoint, notapi/get-signin-status
like before. Consequently, I don't needapp.post('/api/get-signin-status', () => {})
anymore. The reason why I now use theapi/get-user
endpoint is because both did the same thing (i.e., checked the session cookie to see if theuser
property exists in thereq.session
object), which means that only one is enough for my auth app. There's no need to check the session cookie twice.
server.js
/* ... */
/* Removed */
/*
app.post('/api/get-signin-status', async (req, res) => {
try {
if (req.session.user) {
return res.status(200).json({ message: 'User logged in' });
} else {
return res.status(400).json({ message: 'User logged out' });
}
} catch {
return res.status(500).json({ message: 'Internal server error' });
}
});
*/
/* ... */
- I removed my initial code in
auth.service.ts
and added the code as @VonC suggested.
auth.service.ts
/* ... */
/* Removed */
/*
getSignInStatus(data?: any) {
return this.http.post(this.authUrl + 'api/get-signin-status', data, {
withCredentials: true,
});
}
*/
/* Added */
private signInStatus$: BehaviorSubject<any> = new BehaviorSubject<any>(null);
getSignInStatusObserver(): Observable<any> {
return this.signInStatus$.asObservable();
}
setSignInStatus(status: any): void {
this.signInStatus$.next(status);
}
/* ... */
- I removed my initial code in
interceptor.service.ts
and added the code as @VonC suggested. Note: I changed the endpoint fromapi/get-signin-status
toapi/get-user
.
interceptor.service.ts
import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpEvent, HttpRequest, HttpHandler, HttpResponse } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { tap, catchError } from 'rxjs/operators';
import { AuthService } from 'src/app/auth/services/auth.service';
@Injectable({
providedIn: 'root',
})
export class InterceptorService implements HttpInterceptor {
intercept(httpRequest: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(httpRequest).pipe(
tap((event: HttpEvent<any>) => {
if (event instanceof HttpResponse && httpRequest.url.endsWith('api/get-user')) {
this.authService.setSignInStatus({ success: true, response: event.body });
}
}),
catchError((err: any) => {
if (httpRequest.url.endsWith('api/get-user')) {
this.authService.setSignInStatus({ success: false, response: err });
}
return throwError(err);
})
);
}
constructor(private authService: AuthService) {}
}
- I removed my initial code in
header.component.ts
and added the code as @VonC suggested.
header.component.ts
import { Component, OnInit } from '@angular/core';
import { AuthService } from 'src/app/auth/services/auth.service';
import { Router } from '@angular/router';
import { MatSnackBar } from '@angular/material/snack-bar';
@Component({
selector: 'app-header',
templateUrl: './header.component.html',
styleUrls: ['./header.component.scss'],
})
export class HeaderComponent implements OnInit {
signInStatus: any;
constructor(private authService: AuthService, public publicAuthService: AuthService, private signOutRouter: Router, private snackBar: MatSnackBar) {
this.authService.getSignInStatusObserver().subscribe((response: any) => {
this.signInStatus = response;
if (response) {
console.log('Sign in status success:', response.response);
} else {
console.log('Sign in status is null');
}
});
}
ngOnInit(): void {}
}
Now I can finally show elements in header.component.html
depending on sign-in status coming from the backend as follows:
header.component.html
<div *ngIf="signInStatus">Show this element if the user is signed in</div>
<div *ngIf="!signInStatus">Show this element if the user is signed out</div>
答案1
得分: 1
"interceptor response is null" 可能是预期的情况:您在拦截器中使用 RxJS 的 BehaviorSubject
的方式可能导致了这个问题。
拦截器的目的是拦截 HTTP 请求并对这些请求或响应执行某些操作。
(请参见例如 Intro to Angular Http Interceptors 由 Cory Rylan 编写的文章)
但是在您的拦截器中,您通过调用 this.authService.getSignInStatus().subscribe(signInStatusObserver);
创建了一个 新的 HTTP 请求,而不是拦截现有的请求。这意味着当您的组件订阅 getInterceptorResponse()
时,对这个请求的响应可能不会立即可用。
... 因此可能会出现 null 拦截器响应。
举个例子,您可以使用拦截器来检查用户的身份验证状态:
// interceptor.service.ts
import { tap, catchError } from 'rxjs/operators';
@Injectable({
providedIn: 'root',
})
export class InterceptorService implements HttpInterceptor {
constructor(private authService: AuthService) {}
intercept(httpRequest: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(httpRequest).pipe(
tap((event: HttpEvent<any>) => {
if (event instanceof HttpResponse && httpRequest.url.endsWith('/api/get-signin-status')) {
this.authService.setSignInStatus({ success: true, response: event.body });
}
}),
catchError((err: any) => {
if (httpRequest.url.endsWith('/api/get-signin-status')) {
this.authService.setSignInStatus({ success: false, response: err });
}
return throwError(err);
})
);
}
}
在这个更新后的版本中,我们订阅了 next
的 handle
方法,它返回了一个 HTTP 响应的 Observable
。它使用 tap
操作符来处理成功的响应,使用 catchError
操作符来处理错误。
我假设 setSignInStatus
是您的 AuthService
中的一个方法,用于更新存储在 BehaviorSubject
中的登录状态。
我还检查请求的 URL 是否以 '/api/get-signin-status' 结尾,以确保我们只为该特定请求设置登录状态。
这意味着您可以使用以下方式实现用户的身份验证状态:
// auth.service.ts
private signInStatus$: BehaviorSubject<any> = new BehaviorSubject<any>(null);
getSignInStatusObserver(): Observable<any> {
return this.signInStatus$.asObservable();
}
setSignInStatus(status: any): void {
this.signInStatus$.next(status);
}
您需要更新 header.component.ts
,以使用 authService
而不是 interceptorService
:
// header.component.ts
constructor(private authService: AuthService) {
this.authService.getSignInStatusObserver().subscribe((response: any) => {
console.log(response);
this.interceptorResponse = response;
if (response) {
console.log('Interceptor response success:', response.response);
} else {
console.log('Interceptor response is null');
}
});
}
最后,在您的组件中,您应该订阅 getSignInStatusObserver
而不是 getInterceptorResponse
。
这确保了每次从 '/api/get-signin-status' 收到 HTTP 响应时都会更新登录状态,并且组件可以订阅状态更新。
您的组件中的订阅代码如下:
// header.component.ts
import { Component, OnInit } from '@angular/core';
import { AuthService } from '../auth/services/auth.service';
@Component({
selector: 'app-header',
templateUrl: './header.component.html',
styleUrls: ['./header.component.scss'],
})
export class HeaderComponent implements OnInit {
signInStatus: any;
constructor(private authService: AuthService) {
this.authService.getSignInStatusObserver().subscribe((response: any) => {
// 每当 signInStatus 更新时,该代码块将运行
console.log(response);
this.signInStatus = response;
if (response) {
console.log('Sign in status success:', response.response);
} else {
console.log('Sign in status is null');
}
});
}
ngOnInit(): void {}
}
在 HeaderComponent
的构造函数中,我们注入了 AuthService
而不是 InterceptorService
。我们订阅了 getSignInStatusObserver()
,这是 AuthService
中的一个方法,返回一个登录状态的 Observable
。当登录状态更新时,订阅的函数将以新状态作为参数被调用。
我们将登录状态存储在 signInStatus
属性中,并将其记录到控制台。如果响应存在,我们还会记录一个成功的消息,否则记录一个指示登录状态为空的消息。这与您的原始代码的行为相似。
英文:
The "interceptor response is null
" might be expected: the way you are using the RxJS BehaviorSubject
in your interceptor may be causing this issue.
The purpose of an interceptor is to intercept HTTP requests and do something with those requests or responses.
(See for instance "Intro to Angular Http Interceptors" from Cory Rylan)
But in your interceptor, you are creating a new HTTP request by calling this.authService.getSignInStatus().subscribe(signInStatusObserver);
instead of intercepting an existing one. That means the response to this request might not be available immediately when your component subscribes to getInterceptorResponse()
.
... hence possibly the null interceptor response.
As an example, you could use an interceptor to check the user's authentication status:
// interceptor.service.ts
import { tap, catchError } from 'rxjs/operators';
@Injectable({
providedIn: 'root',
})
export class InterceptorService implements HttpInterceptor {
constructor(private authService: AuthService) {}
intercept(httpRequest: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(httpRequest).pipe(
tap((event: HttpEvent<any>) => {
if (event instanceof HttpResponse && httpRequest.url.endsWith('/api/get-signin-status')) {
this.authService.setSignInStatus({ success: true, response: event.body });
}
}),
catchError((err: any) => {
if (httpRequest.url.endsWith('/api/get-signin-status')) {
this.authService.setSignInStatus({ success: false, response: err });
}
return throwError(err);
})
);
}
}
In this updated version, we are subscribing to the handle
method of next
, which returns an Observable
of the HTTP response. It uses the tap
operator to do something with the successful responses and the catchError
operator to handle errors.
I am assuming setSignInStatus
is a method in your AuthService
that updates the sign-in status stored in a BehaviorSubject
.
I am also checking if the request URL ends with '/api/get-signin-status
' to make sure we are only setting the sign-in status for that specific request.
That means you might implement this user's authentication status with:
// auth.service.ts
private signInStatus$: BehaviorSubject<any> = new BehaviorSubject<any>(null);
getSignInStatusObserver(): Observable<any> {
return this.signInStatus$.asObservable();
}
setSignInStatus(status: any): void {
this.signInStatus$.next(status);
}
You would need to update your header.component.ts
to use authService
instead of interceptorService
:
// header.component.ts
constructor(private authService: AuthService) {
this.authService.getSignInStatusObserver().subscribe((response: any) => {
console.log(response);
this.interceptorResponse = response;
if (response) {
console.log('Interceptor response success:', response.response);
} else {
console.log('Interceptor response is null');
}
});
}
Finally, in your component, you would then subscribe to getSignInStatusObserver
instead of getInterceptorResponse
.
That ensures that the sign-in status is updated every time an HTTP response is received from '/api/get-signin-status
' and that components can subscribe to the status updates.
The subscription in your component would look like:
// header.component.ts
import { Component, OnInit } from '@angular/core';
import { AuthService } from '../auth/services/auth.service';
@Component({
selector: 'app-header',
templateUrl: './header.component.html',
styleUrls: ['./header.component.scss'],
})
export class HeaderComponent implements OnInit {
signInStatus: any;
constructor(private authService: AuthService) {
this.authService.getSignInStatusObserver().subscribe((response: any) => {
// That block of code will run every time the signInStatus updates
console.log(response);
this.signInStatus = response;
if (response) {
console.log('Sign in status success:', response.response);
} else {
console.log('Sign in status is null');
}
});
}
ngOnInit(): void {}
}
In the constructor of HeaderComponent
, we are injecting AuthService
instead of InterceptorService
. We are subscribing to getSignInStatusObserver()
, which is a method in AuthService
that returns an Observable
of the sign-in status. When the sign-in status is updated, the subscribed function will be called with the new status as the argument.
We are storing the sign-in status in the signInStatus
property and logging it to the console. We are also logging a success message if the response exists and a message indicating the sign-in status is null otherwise. That mirrors the behavior of your original code.
答案2
得分: 1
我猜问题可能是Angular创建了多个interceptorService的实例。一个用作拦截器,另一个被注入到你的组件/服务中。
我依稀记得我曾经遇到过相同的问题。当时我是这样解决的:
class Interceptor{
constructor(private final service: SharedDataService){}
}
class Component{
constructor(private final service: SharedDataService){}
}
组件和拦截器都使用另一个服务来共享数据。
编辑
快速搜索实际上证实了这一点。请参见https://stackoverflow.com/questions/69268285/interceptor-create-two-instances-of-singleton-service
英文:
I'm guessing the problem is that Angular creates multiple instances of your interceptorService. One actually used as an interceptor and one is injected into your components/services.
I vaguely remember having the same problem. Back then, I solved it like this:
class Interceptor{
constructor(private final service: SharedDataService){}
}
class Component{
constructor(private final service: SharedDataService){}
}
Both the component and interceptor use another service to share data.
EDIT
A quick search actually confirms this. See https://stackoverflow.com/questions/69268285/interceptor-create-two-instances-of-singleton-service
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论