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

Angular 6如何在每次请求中维持JWT会话?

解决Angular 6 JWT令牌自动过期且不刷新的问题

嘿,我看你遇到了JWT会话维持的核心问题——当前的AuthInterceptor只是把本地存储的令牌附在请求上,但完全没处理令牌过期后的自动刷新,也没有在每次请求时重置令牌的有效期。咱们一步步来搞定这个:

1. 先确认后端的必要支持

首先你的NodeJS API需要实现刷新令牌(Refresh Token)机制:

  • 用户登录成功时,除了返回30秒过期的accessToken,还要返回一个有效期更长的refreshToken(比如1天)
  • 提供一个专门的刷新接口(比如/api/refresh-token),接收refreshToken,验证通过后返回新的accessToken(可选更新refreshToken)

2. 改造你的AuthInterceptor

我们需要在Interceptor里加入:令牌过期判断、自动刷新逻辑、并发请求处理(避免多次调用刷新接口)。

首先添加解析JWT过期时间的辅助函数,再修改拦截逻辑:

import { Injectable } from '@angular/core';
import {
  HttpInterceptor,
  HttpRequest,
  HttpHandler,
  HttpEvent,
  HttpResponse
} from '@angular/common/http';
import { Observable, Subject, throwError } from 'rxjs';
import { Router } from '@angular/router';
import { HttpClient } from '@angular/common/http';
import { tap, switchMap, filter, take, catchError } from 'rxjs/operators';

@Injectable()
export class AuthInterceptor implements HttpInterceptor {
  // 标记是否正在刷新令牌,避免并发请求重复刷新
  private refreshTokenInProgress = false;
  // 用来通知等待中的请求新令牌已获取
  private refreshTokenSubject: Subject<any> = new Subject<any>();

  constructor(private router: Router, private http: HttpClient) { }

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    // 跳过不需要认证的请求
    if (req.headers.get('No-Auth') === "True") {
      return next.handle(req.clone());
    }

    const userToken = localStorage.getItem('userToken');
    const refreshToken = localStorage.getItem('refreshToken');

    // 没有令牌直接跳登录
    if (!userToken) {
      this.router.navigateByUrl('/login');
      return next.handle(req);
    }

    // 检查令牌是否即将过期(提前5秒判断,避免请求途中过期)
    if (this.isTokenExpired(userToken)) {
      if (!this.refreshTokenInProgress) {
        this.refreshTokenInProgress = true;
        this.refreshTokenSubject.next(null);

        // 调用后端刷新令牌接口
        return this.http.post('/api/refresh-token', { refreshToken })
          .pipe(
            switchMap((response: any) => {
              this.refreshTokenInProgress = false;
              // 更新本地存储的新令牌
              localStorage.setItem('userToken', response.accessToken);
              // 通知等待的请求可以继续了
              this.refreshTokenSubject.next(response.accessToken);

              // 用新令牌克隆原请求并发送
              const clonedReq = req.clone({
                headers: req.headers.set("Authorization", `Bearer ${response.accessToken}`)
              });
              return next.handle(clonedReq);
            }),
            catchError((error) => {
              this.refreshTokenInProgress = false;
              // 刷新失败,清除令牌跳登录
              localStorage.removeItem('userToken');
              localStorage.removeItem('refreshToken');
              this.router.navigateByUrl('/login');
              return throwError(error);
            })
          );
      } else {
        // 如果正在刷新,等待新令牌获取后再处理当前请求
        return this.refreshTokenSubject.pipe(
          filter(token => token !== null),
          take(1),
          switchMap(token => {
            const clonedReq = req.clone({
              headers: req.headers.set("Authorization", `Bearer ${token}`)
            });
            return next.handle(clonedReq);
          })
        );
      }
    } else {
      // 令牌有效,正常发送请求
      const clonedReq = req.clone({
        headers: req.headers.set("Authorization", `Bearer ${userToken}`)
      });
      return next.handle(clonedReq)
        .pipe(
          tap(event => {
            if (event instanceof HttpResponse) {
              // 修正之前的时间计算错误,正确统计请求耗时
              const requestStartTime = new Date().getTime();
              const elapsed = Date.now() - requestStartTime;
              console.log(`Request for ${req.urlWithParams} took ${elapsed} ms.`);
            }
          }, error => {
            console.error('Request error:', error);
            // 如果后端返回401,强制跳登录
            if (error.status === 401) {
              localStorage.removeItem('userToken');
              this.router.navigateByUrl('/login');
            }
          })
        );
    }
  }

  // 解析JWT获取过期时间
  private getTokenExpirationDate(token: string): Date | null {
    try {
      // JWT的payload是Base64编码的第二部分
      const decodedPayload = JSON.parse(atob(token.split('.')[1]));
      if (!decodedPayload.exp) return null;
      const expirationDate = new Date(0);
      expirationDate.setUTCSeconds(decodedPayload.exp);
      return expirationDate;
    } catch (e) {
      // 解析失败视为令牌无效
      return null;
    }
  }

  // 判断令牌是否即将过期
  private isTokenExpired(token: string): boolean {
    const expirationDate = this.getTokenExpirationDate(token);
    if (!expirationDate) return true;
    // 提前5秒触发刷新,避免请求刚好在发送时过期
    return expirationDate.getTime() - new Date().getTime() < 5000;
  }
}

3. 关键注意事项

  • Refresh Token的存储安全:如果你的应用对安全性要求高,建议把refreshToken存在HttpOnly Cookie里(前端无法读取),由后端处理刷新逻辑,避免XSS攻击窃取令牌
  • 后端刷新接口的安全:要验证refreshToken的有效性,比如和用户绑定、设置过期时间,避免被滥用
  • 并发请求处理:上面的代码用refreshTokenInProgress和refreshTokenSubject确保多个请求同时发现令牌过期时,只会调用一次刷新接口,其他请求等待新令牌
  • 令牌过期后的降级处理:如果refreshToken也过期了,直接清除本地存储并跳转登录页面

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.29 08:19:43