如何在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绑定,避免前端存储的风险:
- 后端修改
- 登录时,后端自行生成CSRF Token(如
crypto.randomUUID()) - 将Token存入用户session(需配合
express-session等中间件),同时将Token通过响应体返回给前端 - session cookie保持HTTP-only,CSRF Token无需存入cookie
- 登录时,后端自行生成CSRF Token(如
- 前端修改
- 登录成功后,将后端返回的CSRF Token存入
localStorage或sessionStorage - 每次请求时,在请求头(如
X-CSRF-Token)中携带该Token
- 登录成功后,将后端返回的CSRF Token存入
- 验证逻辑
- 后端接收请求时,从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的问题:
- 后端修改
- 将
csrfTokencookie的httpOnly属性设为false,允许前端读取
- 将
- 前端修改
- 初始化时,先尝试从cookie中读取
csrfToken,如果存在则直接使用;如果不存在再生成新的 - 将Token存入
localStorage,避免页面刷新/浏览器重启后丢失 - 每次请求时,同时携带Token(请求体/请求头),后端对比请求中的Token与cookie中的Token是否一致
- 初始化时,先尝试从cookie中读取
示例前端修改(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
相关产品推荐
相关产品推荐

