You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Angular刷新请求NodeJS后端返回200但无Token问题排查

页面刷新时Angular请求后端Token异常问题

页面刷新时,Angular前端向NodeJS后端发起HTTP请求获取Token,偶尔会出现请求被取消的情况,甚至返回200状态码也收不到Token。但用Postman请求,或者在Angular构造函数里直接调用该请求都能正常拿到Token,怀疑是RxJS使用问题,但找不到具体原因。


AppComponent中的刷新逻辑

constructor(
private router: Router,
private auth: AuthService
) {
// this.auth.requestServer(ServerMethod.GET, '/refresh').subscribe() // 直接调用此请求正常

router.events.pipe(
  switchMap((event) => {
    if (event instanceof NavigationStart) {
      const browserRefresh = !router.navigated;
      if (browserRefresh) {
        this.auth.deleteAccessToken();
        return this.auth.requestServer(ServerMethod.GET, '/refresh');
      }
    }
    return EMPTY;
  })
).subscribe();
}

deleteAccessToken方法

deleteAccessToken() {
    sessionStorage.removeItem('accessToken');
    this.tokenSubject.next(null);
}

requestServer方法

requestServer(type: ServerMethod, path?: string, data?: any): Observable<any> {
    let httpOptions: httpOptions;
    switch (type) {
      case ServerMethod.POST:
        return this.server.post(path, data).pipe(tap(res => this.handleAccessToken(res)));
      case ServerMethod.GETALL:
        return this.server.getAll(this.getAllPath);
      case ServerMethod.GET:
        return this.server.get(path).pipe(tap(res => this.handleAccessToken(res)));
      default:
        return EMPTY;
    }
}

Server服务中的get方法

get(path: string): Observable<any> {
    const url = this.serverAdress + path;
    return this.http.get(url);
}

NodeJS后端refresh接口逻辑

module.exports.refresh_get = async (req, res) => {
    if (req.cookies && req.cookies.refreshToken) {

        // 从cookie中取出refreshToken
        const refreshToken = req.cookies.refreshToken;

        // 验证refresh token
        jwt.verify(refreshToken, 'secret',
            (err, decodedToken) => {
                if (err) {

                    // 无效的Refresh Token
                    res.status(406).json({ message: 'wrong refresh token' });
                }
                else {
                    // 生成新的accessToken
                    const accessToken = createToken(decodedToken.id);
                    // 生成新的refreshToken并设置到cookie中
                    const newRefreshToken = jwt.sign({
                        id: decodedToken.id,
                    }, 'secret', { expiresIn: '1d' });
                    res.cookie('refreshToken', newRefreshToken, { httpOnly: true,
                        sameSite: 'None', secure: true,
                        maxAge: 24 * 60 * 60 * 1000 });
                    res.status(200).json({ accessToken });
                }
            })
    } else {
        res.status(406).json({ message: 'Unauthorized' });
    }
}

问题原因与解决方案

核心原因

  1. switchMap的取消特性:switchMap会在新事件触发时取消未完成的Observable,页面刷新时路由事件可能多次触发,导致当前HTTP请求被中途取消。
  2. 路由事件时序误差:router.navigated状态在页面刷新初期不稳定,可能导致浏览器刷新的判断逻辑失效,请求提前终止。
  3. 跨域凭证未携带:Angular HTTP请求默认不携带cookie,若后端依赖cookie中的refreshToken,会导致逻辑执行异常,返回空响应。
  4. 后端异步逻辑混乱:后端使用async但嵌套回调函数,可能导致响应未正确发送。

针对性解决方案

1. 修正RxJS操作符与事件触发逻辑

用filter+first()确保只处理一次页面刷新的NavigationStart事件,避免重复触发导致请求被取消:

router.events.pipe(
  filter(event => event instanceof NavigationStart),
  first(), // 仅取首次路由启动事件
  switchMap((event) => {
    const browserRefresh = !router.navigated;
    if (browserRefresh) {
      this.auth.deleteAccessToken();
      return this.auth.requestServer(ServerMethod.GET, '/refresh');
    }
    return EMPTY;
  })
).subscribe({
  next: res => console.log('Token获取成功', res),
  error: err => console.error('请求失败', err)
});

2. 确保HTTP请求携带跨域凭证

修改Server服务的get方法,添加withCredentials: true,让请求携带cookie:

get(path: string): Observable<any> {
    const url = this.serverAdress + path;
    return this.http.get(url, { withCredentials: true }); // 开启凭证携带
}

3. 优化后端异步逻辑

改用util.promisify将jwt.verify转为Promise,配合async/await避免回调嵌套导致的响应异常:

const jwt = require('jsonwebtoken');
const util = require('util');
const verifyToken = util.promisify(jwt.verify);

module.exports.refresh_get = async (req, res) => {
    try {
        if (!req.cookies?.refreshToken) {
            return res.status(406).json({ message: 'Unauthorized' });
        }

        const refreshToken = req.cookies.refreshToken;
        const decodedToken = await verifyToken(refreshToken, 'secret');
        
        const accessToken = createToken(decodedToken.id);
        const newRefreshToken = jwt.sign({ id: decodedToken.id }, 'secret', { expiresIn: '1d' });
        
        res.cookie('refreshToken', newRefreshToken, { 
            httpOnly: true,
            sameSite: 'None', 
            secure: true,
            maxAge: 24 * 60 * 60 * 1000 
        });
        res.status(200).json({ accessToken });
    } catch (err) {
        res.status(406).json({ message: 'wrong refresh token' });
    }
}

4. 管理订阅生命周期

在组件中保存订阅对象,避免内存泄漏和意外取消:

private refreshSubscription: Subscription;

constructor(
private router: Router,
private auth: AuthService
) {
this.refreshSubscription = router.events.pipe(
  filter(event => event instanceof NavigationStart),
  first(),
  switchMap((event) => {
    const browserRefresh = !router.navigated;
    if (browserRefresh) {
      this.auth.deleteAccessToken();
      return this.auth.requestServer(ServerMethod.GET, '/refresh');
    }
    return EMPTY;
  })
).subscribe();
}

ngOnDestroy() {
  this.refreshSubscription?.unsubscribe();
}

内容的提问来源于stack exchange,提问作者Mike Twain

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.31 14:16:47