Token 时代的黎明 —— 从简单 Token 到 JWT 的崛起(2012–2018)

6 阅读5分钟

上一章我们聊了 Cookie + Session 如何统治 Web 黄金十年(1998–2012)。但进入 2010 年代,移动 App、SPA(Single Page Application)、前后端分离、微服务这些新事物像潮水一样涌来,把 Session 模式逼到了墙角。

于是,Token 开始登场。从最原始的“随便传个字符串”到 2015 年正式成为 RFC 的 JSON Web Token (JWT),前端第一次真正开始“接管”登录状态。这是一个从混乱到标准化的黎明期,也是前端工程师从“表单提交仔”变成“状态管理大师”的起点。

1. Session 模式在 2012–2014 年暴露的致命伤

  • 跨域携带麻烦:浏览器默认不跨域带 Cookie(CORS + withCredentials 配对很麻烦),移动端 / 原生 App 更不可能靠 Cookie
  • 服务器有状态:Session 存在内存/文件/Redis,集群时要么粘性 session(负载均衡痛苦),要么全量 Session 共享(Redis 成为瓶颈)
  • 移动 + API 优先:iOS/Android App 需要纯 JSON API,Cookie 不是首选
  • SPA 崛起:AngularJS(2010–2012 爆火)、Backbone、Ember 等框架让页面不刷新,前端必须自己管理“已登录”状态

工程师们开始尝试最原始的方案:简单 Token

2. 简单 Token 时代(2012–2014):随便传个字符串

典型做法(伪代码):

后端登录成功后:

// PHP / Node / Java 随便写
const token = uuid.v4();  // 或 md5(userId + salt + timestamp)
redis.set(`token:${token}`, JSON.stringify({ userId: 123, expire: ts + 7*86400 }), 'EX', 7*86400);
res.json({ token });

前端(jQuery / AngularJS 早期):

// 存 localStorage
localStorage.setItem('auth_token', response.token);

// 每个请求手动加 header
$.ajax({
  url: '/api/user',
  headers: { 'Authorization': 'Bearer ' + localStorage.getItem('auth_token') }
});

优点:简单、无状态(校验时查 Redis 就行)、跨域友好。

缺点:

  • 完全不透明:前端不知道过期时间、权限等
  • 无法防篡改(明文或简单 hash)
  • 登出/强制下线?必须后端维护黑名单
  • 安全性低:泄露即永久有效(除非短有效期 + 刷新机制)

这个阶段很多中小项目、内部系统、早期移动端 API 都这么干。代表:2012–2014 年的大量 RESTful API 项目。

3. JWT 横空出世:2010 年草案 → 2015 年 RFC 7519

JWT 的故事其实从 2010 年底就开始了:

  • 2010 年 12 月:第一个 Internet-Draft 出现(draft-jones-json-web-token-01)
  • 2011–2014:多次迭代,主要是 Mike Jones(微软)、John Bradley、Nat Sakimura 等人在 IETF OAuth 工作组推动
  • 2013 年:Auth0 等公司开始大力宣传和实现早期草案版本(jwt.io 网站雏形出现)
  • 2015 年 5 月:正式发布 RFC 7519(JSON Web Token),同时配套 RFC 7515 (JWS)、7516 (JWE)、7517 (JWK)、7518 (JWA)

JWT 结构一目了然(三段 Base64Url + . 分隔):

header.payload.signature
  • Header:算法、类型(通常 {"alg":"HS256","typ":"JWT"}
  • Payload:Claims(标准字段如 sub, iat, exp, iss, aud + 自定义字段)
  • Signature:用密钥对 header+payload 签名(HS256 对称、RS256 非对称)

典型 JWT 示例(jwt.io 上调试神器):

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

2015 年后,jwt.io 网站上线 + 大量库爆发(jsonwebtoken Node、jjwt Java、PyJWT Python、angular-jwt 等),JWT 迅速成为事实标准。

4. JWT 为什么在 2015–2018 年爆火?(SPA + 微服务完美契合)

  • 无状态:服务端不存 Session,校验只看签名 + exp + iss/aud → 天生适合微服务、Serverless、CDN 边缘验证
  • 自包含:Payload 带用户信息、权限、过期时间,前端可直接解析(不需每次问后端“你是谁”)
  • 跨域 / 多端友好:Bearer Token 放 Authorization header,移动端、Postman、浏览器全通用
  • 前端存储灵活:localStorage / sessionStorage / memory / cookie(httpOnly 也可,但少用)
  • 生态爆发:2015–2016 年 Angular 1.x + angular-jwt、React + redux、Vue 早期项目大量采用

典型前端使用(2016–2018 年主流写法):

// axios 拦截器(最经典)
import axios from 'axios';

axios.interceptors.request.use(config => {
  const token = localStorage.getItem('jwt');
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

// 登录后存储
function loginSuccess(res) {
  localStorage.setItem('jwt', res.data.token);
  // 或用 vuex / redux 存(推荐,方便管理)
}

5. 早期 JWT 的两大存储之争(2015–2018)

存储位置优点缺点 / 风险主流选择时期
localStorage简单、跨标签页共享、持久XSS 可窃取(document 可读)2015–2017 大量用
sessionStorage标签页隔离、不持久刷新丢失、仍 XSS 风险少用
HttpOnly Cookie防 XSS(JS 读不到)、浏览器自动带跨域麻烦、CSRF 风险、SameSite 未普及保守派 / ToB 系统
内存(vuex/redux)只在当前页面、结合 refresh token刷新丢失、需配合持久化插件2017–2018 渐主流

结论:2015–2017 年 localStorage + JWT 是最流行组合,被吐槽最多的是 XSS 风险(但当时很多项目没太在意)。

6. 早期 JWT 的安全坑与教训(2015–2018 高发)

  • alg: none 漏洞(2015 年发现,多库未过滤)
  • RS256 公钥混淆攻击(2016–2017 爆出 alg confusion)
  • 弱密钥 HS256(2017 前常见)
  • 不校验 iss / aud / exp(很多项目只验签名)
  • 永不过期或超长 exp(常见安全事故源)

缓解:用成熟库(jsonwebtoken v8+、auth0 的库)、强制 alg 白名单、短有效期 + refresh token。

7. 小结:2012–2018 是 Token 时代的“青春期”

  • 2012–2014:简单 Token 试水,无标准、无库
  • 2015:JWT RFC 诞生,jwt.io 上线,爆发起点
  • 2016–2018:SPA + 前后端分离全面拥抱 JWT,成为中后台、移动 API、微服务标配

但 JWT 也不是银弹:

  • 无法主动失效(除非黑名单 / 短有效期)
  • Payload 可读(Base64 不是加密)
  • XSS / 泄露风险永久存在