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

如何在Firebase、Angular与Express中正确处理CSRF Token?

CSRF Token实现问题及解决方案

问题描述

当前CSRF Token实现逻辑:

  • Angular前端登录时生成随机UID作为CSRF Token,与Firebase ID Token一同发送至后端
  • 后端通过ID Token生成session cookie,并将session和CSRF Token存入HTTP-only cookie
  • 后续请求时,前端携带UID,后端对比cookie中的CSRF Token是否匹配

核心问题:
session cookie有效期为两周(关闭浏览器仍保留),但前端的CSRF Token仅存于内存中,关闭浏览器后丢失。重新打开页面时前端生成新的UID,与cookie中留存的旧Token不匹配,导致请求被拒绝。

当前实现代码

后端(Express)

import { Router } from "express";
import { getAuth } from "firebase-admin/auth";
const sessionRouter = Router();

sessionRouter.post("/", (req, res) => {
  // 获取传入的ID Token和CSRF Token
  const idToken = req.body.idToken.toString();
  const csrfToken = req.body.csrfToken.toString();
  // 防御CSRF攻击
  if (!csrfToken) {
    res.status(401).send("UNAUTHORIZED REQUEST!");
    return;
  }
  // 设置会话有效期为14天
  const expiresIn = 60 * 60 * 24 * 14 * 1000;
  // 创建会话Cookie,同时验证ID Token
  getAuth()
    .createSessionCookie(idToken, { expiresIn })
    .then(
      (sessionCookie) => {
        // 设置Cookie策略
        const options = {
          maxAge: expiresIn,
          httpOnly: true,
          secure: false,
          samesite: "None",
        };
        res.cookie("session", sessionCookie, options);
        res.cookie("csrfToken", csrfToken, options);
        res.end(JSON.stringify({ status: "success" }));
      },
      (error) => {
        res.status(401).send("UNAUTHORIZED REQUEST!");
      }
    );
});

export default sessionRouter;

前端(Angular)

FirebaseAuthService

import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { environment } from '../../environments/environment.development';
import { initializeApp } from 'firebase/app';
import {
  getAuth,
  setPersistence,
  inMemoryPersistence,
  signInWithEmailAndPassword,
  UserCredential,
} from 'firebase/auth';
import { UuidService } from './uuid.service';
import { RequestService } from './request.service';

@Injectable({
  providedIn: 'root',
})
export class FirebaseAuthService {
  app = initializeApp(environment.firebaseConfig);
  auth = getAuth(this.app);

  constructor(
    private uuidService: UuidService,
    private router: Router,
    private requestService: RequestService
  ) {
    setPersistence(this.auth, inMemoryPersistence);
  }

  async signIn(email: string, password: string) {
    try {
      const userCredential: UserCredential = await signInWithEmailAndPassword(
        this.auth,
        email,
        password
      );
      const csrfToken = this.uuidService.uuidValue;
      const idToken = await userCredential.user.getIdToken();
      this.requestService.postIdTokenToSessionLogin(idToken, csrfToken);
    } catch (error) {}
  }
}

RequestService

import { Injectable } from '@angular/core';
import { environment } from '../../environments/environment.development';

@Injectable({
  providedIn: 'root',
})
export class RequestService {
  URL = environment.url;
  HEADERS = {
    'Content-Type': 'application/json',
  };
  constructor() {}

  async postIdTokenToSessionLogin(idToken: string, csrfToken: string) {
    try {
      const response = await fetch(`${this.URL}/sessionLogin`, {
        method: 'POST',
        headers: this.HEADERS,
        credentials: 'include',
        body: JSON.stringify({ idToken, csrfToken }),
      });
      const data = await response.json();
      console.log('服务器响应:', data);
    } catch (error) {}
  }
}

UuidService

import { Injectable } from '@angular/core';
import { v4 as uuidv4 } from 'uuid';

@Injectable({
  providedIn: 'root',
})
export class UuidService {
  uuid = '';

  constructor() {
    this.uuid = uuidv4();
  }

  get uuidValue() {
    return this.uuid;
  }
}

正确实现思路与修复方案

核心问题分析

当前方案的致命缺陷是:CSRF Token由前端生成并仅存于内存,而对应的cookie是HTTP-only(前端无法读取),导致浏览器重启后前端无法获取旧Token,与cookie中留存的Token不匹配。

方案一:后端生成并管理CSRF Token(推荐)

这种方式更安全,后端将Token与用户session绑定,避免前端存储的风险:

  1. 后端修改
    • 登录时,后端自行生成CSRF Token(如crypto.randomUUID())
    • 将Token存入用户session(需配合express-session等中间件),同时将Token通过响应体返回给前端
    • session cookie保持HTTP-only,CSRF Token无需存入cookie
  2. 前端修改
    • 登录成功后,将后端返回的CSRF Token存入localStorage或sessionStorage
    • 每次请求时,在请求头(如X-CSRF-Token)中携带该Token
  3. 验证逻辑
    • 后端接收请求时,从session中取出CSRF Token,与请求头中的Token对比,一致则通过验证

示例后端修改片段:

// 需先配置express-session
getAuth()
  .createSessionCookie(idToken, { expiresIn })
  .then((sessionCookie) => {
    const options = {
      maxAge: expiresIn,
      httpOnly: true,
      secure: false,
      samesite: "None",
    };
    res.cookie("session", sessionCookie, options);
    // 生成并存储CSRF Token到session
    const csrfToken = crypto.randomUUID();
    req.session.csrfToken = csrfToken;
    // 返回Token给前端
    res.end(JSON.stringify({ status: "success", csrfToken }));
  });

方案二:优化Double Submit Cookie模式

如果坚持使用Double Submit模式,需解决前端无法读取Cookie的问题:

  1. 后端修改
    • 将csrfToken cookie的httpOnly属性设为false,允许前端读取
  2. 前端修改
    • 初始化时,先尝试从cookie中读取csrfToken,如果存在则直接使用;如果不存在再生成新的
    • 将Token存入localStorage,避免页面刷新/浏览器重启后丢失
    • 每次请求时,同时携带Token(请求体/请求头),后端对比请求中的Token与cookie中的Token是否一致

示例前端修改(UuidService):

import { Injectable } from '@angular/core';
import { v4 as uuidv4 } from 'uuid';

@Injectable({
  providedIn: 'root',
})
export class UuidService {
  uuid = '';

  constructor() {
    // 先尝试从cookie读取
    const savedCsrfToken = this.getCookie('csrfToken');
    if (savedCsrfToken) {
      this.uuid = savedCsrfToken;
    } else {
      this.uuid = uuidv4();
    }
  }

  get uuidValue() {
    return this.uuid;
  }

  private getCookie(name: string): string | null {
    const value = `; ${document.cookie}`;
    const parts = value.split(`; ${name}=`);
    if (parts.length === 2) return parts.pop()?.split(';').shift() || null;
    return null;
  }
}

额外注意事项

  • 生产环境中,secure属性需设为true,确保cookie仅通过HTTPS传输
  • SameSite属性建议设为Lax或Strict,进一步降低CSRF风险
  • 定期刷新CSRF Token,比如用户每次重新登录或每隔一段时间生成新Token,提升安全性

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.27 16:05:16