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

Angular客户端Cookie存储JWT的令牌刷新流程实现咨询

JWT Cookie存储模式下Angular令牌刷新流程实现问题

我刚接触将JWT存储在Cookie中的会话管理方案,现在要在Angular前端实现令牌刷新流程。技术栈为后端Node.js+Express,前端Angular。

后端已完成Token Controller的refreshTokenValidation方法用于刷新令牌,CORS配置开启了credentials: true,登录成功后会通过Set-Cookie响应头把JWT(键名JWT)和刷新令牌(键名JWTREF)存入客户端Cookie。

目前Angular端设置了路由守卫ServicesGuard,进入视图时会调用后端刷新令牌接口但未生效。我不想在客户端存储令牌,不清楚如何访问这些Cookie里的令牌来完成后端的刷新或验证,求解决方案。


后端相关代码

refreshTokenValidation方法

public async refreshTokenValidation(
    request: Request,
    response: Response
  ): Promise<Response> {
    const refreshTokenInCookies = request.cookies["JWTREF"];
    
      if (!refreshTokenInCookies) {
        return response
          .status(400)
          .json(
            RESPONSE_HANDLER.responseHandler(
              false,
              "There is not refresh token available",
              null,
              null,
              null,
              response
            )
          );
      }

      const { userId, userInfo } = JWT_HELPER.getRefreshTokenInfo(
        refreshTokenInCookies
      );

      if (!userId) {
        return response
          .status(400)
          .json(
            ERROR_EXCEPTION_HANDLER.errorHandler(
              false,
              "Token Credentials don´t contain user ID"
            )
          );
      }

      if (!userInfo) {
        return response
          .status(400)
          .json(
            ERROR_EXCEPTION_HANDLER.errorHandler(
              false,
              "Token Credentials don´t contain user INFO"
            )
          );
      }

      await JWT_HELPER.generateJWTandJWTRef({ userId, userInfo });

      return response
        .status(200)
        .json(
            RESPONSE_HANDLER.responseHandler(
              true,
              "Token updated",
              null,
              null,
              null,
              response
            )
          );
    } catch (error) {
      return response
        .status(500)
        .json(ERROR_EXCEPTION_HANDLER.errorHandler(false, error as string));
    }
  }

CORS配置

app.use(
    cors({
      origin: process.env.GOSSIPI_FRONT_ALLOWED_ORIGIN,
      methods: JSON.parse(process.env.ALLOWED_HTTPS_METHODS as string),
      credentials: true, 
      allowedHeaders: [
        "Origin",
        "X-Requested-With",
        "Content-Type",
        "Accept",
        "Authorization",
      ],
    })
  );

登录接口loginUser方法

public async loginUser(
    request: Request,
    response: Response
  ): Promise<Response> {
    const data = request.body;

    try {
      const { searchString, password } = data;
      const userWithValidCredentials =
        await FIELDS_VALIDATOR.loginCredentialsAreValid(searchString, password);

      if ((userWithValidCredentials as UserInterface) === undefined) {
        return response
          .status(400)
          .json(
            ERROR_EXCEPTION_HANDLER.errorHandler(
              false,
              "This user name,email, or nick is incorrect"
            )
          );
      }

      const { token, refreshToken } =
        (await FIELDS_VALIDATOR.jwtGeneratedOnUserValidcredentials(
          userWithValidCredentials
        )) as Record<string, string | null | undefined>;

      if (!token || !refreshToken) {
        return response
          .status(400)
          .json(
            ERROR_EXCEPTION_HANDLER.errorHandler(
              false,
              "The token generated for validation is not valid"
            )
          );
      }

      return response
        .status(200)
        .json(
            RESPONSE_HANDLER.responseHandler(
              true,
              "User has been logged",
              userWithValidCredentials,
              token,
              refreshToken,
              response
            )
          );
    } catch (error) {
      return response
        .status(500)
        .json(
            ERROR_EXCEPTION_HANDLER.errorHandler(
              false,
              "An error occurred while login the user",
              { error }
            )
          );
    }
  }

ResponseHandlerClass类

import { Response } from "express";
import { ResponseInterface } from "../root-interfaces/response-interface/response-interface.interface";

export class ResponseHandlerClass {
  private static responseHandlerInstance: ResponseHandlerClass;

  public static getResponseHandlerInstance(): ResponseHandlerClass {
    if (!this.responseHandlerInstance) {
      this.responseHandlerInstance = new ResponseHandlerClass();
    }

    return this.responseHandlerInstance;
  }

  public responseHandler(
    status: boolean,
    message: string,
    data?: undefined | null | Record<string, any>,
    token?: undefined | null | string,
    refreshToken?: undefined | null | string,
    response?: Response | null
  ): ResponseInterface {
    this.responseCookiesHandler(response ?? null, token, refreshToken);

    return { status, message, data };
  }

  public responseCookiesHandler(
    response: Response | null,
    token: undefined | null | string,
    refreshToken: undefined | null | string
  ): void {
    if (token && refreshToken && response) {
      response?.cookie("JWT", token, {
        httpOnly: true,
        secure: true,
        sameSite: "strict",
        maxAge: 32400000, // 9 hours in milliseconds in order to mach token expirein time,
        domain: "",
      });

      response.cookie("JWTREF", refreshToken, {
        httpOnly: true,
        secure: true,
        sameSite: "strict",
        maxAge: 129600000, // 36 hours in milliseconds  in order to mach refresh token expirein time
        domain: "",
      });
    }
  }
}

Angular前端相关代码

ServicesGuard路由守卫

import { HttpClient } from '@angular/common/http';
import { inject } from '@angular/core';
import {
  ActivatedRouteSnapshot,
  CanActivateFn,
  Router,
  RouterStateSnapshot,
} from '@angular/router';
import { Observable, catchError, map, of } from 'rxjs';
import { environment } from 'src/environments/environment';

export const ServicesGuard: CanActivateFn = (
  route: ActivatedRouteSnapshot,
  state: RouterStateSnapshot
) => {
  const httpClient = inject(HttpClient);
  const router = inject(Router);

  const refreshToken = (): Observable<boolean> => {
    return httpClient
      .post<boolean>(
        `${environment.SERVER_URL_HOST_CONNECTION}/token/token_validation/refresh`,
        {},
        { withCredentials: true }
      )
      .pipe(
        map(() => true),
        catchError(() => {
          router.navigate(['/goss_login']);

          return of(false);
        })
      );
  };

  return refreshToken();
};

路由配置

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { TabConfigPage } from './xxxxe';
import { ServicesGuard } from 'xxxxx';

const routes: Routes = [
  {
    path: '',
    component: TabConfigPage,
    canActivate: [ServicesGuard],
  },
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule],
})
export class TabConfigPageRoutingModule {}

解决方案

1. 核心逻辑确认

你不需要手动访问Cookie里的令牌——Angular请求中设置{ withCredentials: true }后,浏览器会自动把当前域名下的HttpOnly Cookie(包括JWT和JWTREF)携带到后端请求中,完全满足你“不在客户端存储令牌”的需求。

2. 修复后端刷新接口的关键bug

当前refreshTokenValidation方法调用responseHandler时,传入的token和refreshToken为null,导致responseCookiesHandler不会执行,新生成的令牌无法写入Cookie,这是守卫失效的核心原因。修正代码:

// 刷新接口中生成新令牌后,传入responseHandler
const { token: newToken, refreshToken: newRefreshToken } = await JWT_HELPER.generateJWTandJWTRef({ userId, userInfo });

return response
  .status(200)
  .json(
      RESPONSE_HANDLER.responseHandler(
        true,
        "Token updated",
        null,
        newToken,
        newRefreshToken,
        response
      )
    );

3. 排查Cookie配置问题

  • Domain配置:后端ResponseHandlerClass中Cookie的domain为空,生产环境需设置为前端实际域名(如.example.com适配子域名),否则浏览器可能拒绝存储Cookie。
  • Secure属性:开发环境(HTTP)下需把secure改为false,仅HTTPS环境才启用secure: true。
  • SameSite属性:若前后端为不同子域名,建议把sameSite: "strict"改为sameSite: "lax",避免跨站请求时Cookie不被携带。

4. 验证流程

  1. 登录后在浏览器开发者工具Application > Cookies中确认JWT、JWTREF已存储,且属性符合预期。
  2. 触发路由守卫时,查看Network面板的请求头,确认包含Cookie: JWT=xxx; JWTREF=xxx。
  3. 查看刷新接口的响应头,确认存在新的Set-Cookie,更新JWT和JWTREF的值。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.17 19:47:32