HTTP 是健忘的,而程序员是执着的。
我们为让服务器记住用户,造出了两种记忆体系:Session——稳重老派;JWT——灵动新潮。
一个靠“查档案”,一个靠“签契约”。


一、鉴权的核心哲学

HTTP 是无状态的,每一次请求都是陌生人上门:

“我是谁?你凭什么信我?”

于是程序员发明了两种记忆机制:

流派 思想核心
Session 派 服务端保存状态:我记得你是谁。
JWT 派 客户端自带凭证:你自己证明你是谁。

无论选哪派,核心过程都一样:

  1. 用户登录成功,获得凭证。
  2. 每次请求带上凭证。
  3. 服务端验证凭证是否可信。

二、Session:服务器的温柔记忆

🌱 单机版原理与代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 登录接口示例
@PostMapping("/login")
public ResponseEntity<?> login(@RequestParam String username, @RequestParam String password, HttpServletRequest request) {
if ("neo".equals(username) && "123456".equals(password)) {
HttpSession session = request.getSession(); // 若无则创建
session.setAttribute("user", username);
session.setMaxInactiveInterval(30 * 60); // 30分钟超时
return ResponseEntity.ok("登录成功,SessionID=" + session.getId());
} else {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("用户名或密码错误");
}
}

// 受保护接口
@GetMapping("/profile")
public ResponseEntity<?> profile(HttpServletRequest request) {
HttpSession session = request.getSession(false); // 不存在则返回null
if (session != null && session.getAttribute("user") != null) {
return ResponseEntity.ok("你好, " + session.getAttribute("user"));
} else {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("请先登录");
}
}

🔁 Session 生命周期

  • 创建阶段: 第一次调用 getSession()

  • 活跃阶段: 每次请求自动刷新过期时间。

  • 销毁阶段:

    • 超时未访问(默认30分钟)。
    • 主动调用 session.invalidate()
    • 服务器重启或内存清除。

配置示例:

1
server.servlet.session.timeout: 60m

⚙️ 分布式 Session:共享记忆

单机没问题,但一旦负载均衡——

1
2
用户第一次访问 -> Server A(创建Session)
第二次访问 -> Server B(查不到Session)

解决方案三选一:

方案 思路 优点 缺点
Session复制 节点同步Session 应用透明 同步代价大
粘性会话 用户固定访问同一节点 实现简单 容灾差
Session共享(推荐) 使用Redis集中存储Session 稳定可扩展 有外部依赖

Redis共享Session示例:

1
2
3
4
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
1
2
3
4
5
6
spring:
session:
store-type: redis
redis:
host: localhost
port: 6379

Redis 实际存储内容:

1
spring:session:sessions:abc123 -> {"user":"neo", "expireTime":1736971200000}

不一定。

方式 说明 优点
Cookie(默认) 浏览器自动携带 简洁,安全性高,可加 HttpOnlySecure
Header 手动在请求头中传递 Authorization: Session abc123 适合跨域和移动端
URL 参数 ?sid=abc123 ⚠️ 极不安全,禁止用于生产

✅ 结论:SessionID 只是凭证,可以放任何地方,只要服务端能取到即可。

Cookie 不是必须的,只是“最懂浏览器的信使”。


三、JWT:客户端的自由契约

JWT(JSON Web Token)主张:

“状态我自己带,信任靠签名。”

🧩 结构拆解

JWT由三部分组成:

1
Header.Payload.Signature
  • Header:说明算法与类型,例如:

    1
    {"alg": "HS256", "typ": "JWT"}
  • Payload:承载用户数据:

    1
    {"sub": "neo", "role": "admin", "exp": 1760978291}
  • Signature:签名,防止被篡改。

生成Token示例:

1
2
3
4
5
6
7
8
String token = Jwts.builder()
.setSubject(user.getUsername())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + 3600_000))
.claim("role", "admin")
.signWith(SignatureAlgorithm.HS256, SECRET_KEY)
.compact();
response.setHeader("Authorization", "Bearer " + token);

验证Token:

1
2
3
4
5
6
7
8
9
10
11
12
13
try {
Claims claims = Jwts.parser()
.setSigningKey(SECRET_KEY)
.parseClaimsJws(token.replace("Bearer ", ""))
.getBody();
String username = claims.getSubject();
String role = (String) claims.get("role");
System.out.println(username + " is an " + role);
} catch (ExpiredJwtException e) {
System.out.println("Token 已过期");
} catch (JwtException e) {
System.out.println("无效的 Token");
}

🔐 JWT 进阶用法

  • 自定义 Claim: 添加业务信息,如用户ID、登录设备。
  • Token 刷新机制: Access Token + Refresh Token 双Token模式。
  • 黑名单策略: Redis存储已登出的Token,防止重放攻击。

黑名单验证示例:

1
2
3
if (redisTemplate.hasKey("blacklist:" + token)) {
throw new JwtException("Token 已被列入黑名单");
}

四、Session vs JWT:同源不同命

对比项 Session JWT
存储位置 服务端(内存/Redis) 客户端(Token中)
状态管理 有状态 无状态
主动失效 ✅ 可删Session ❌ 需黑名单
分布式支持 需共享Session 天然支持
安全性 高(服务端控制) 中(签名验证)
性能 需查存储 快,直接验签
Token大

Session 是“记得你”;JWT 是“信你自己”。


五、实战选型建议

场景 推荐方案 理由
单体系统 Session 简单稳健
分布式系统 Redis Session 高可用、低风险
前后端分离 JWT 无状态、跨域支持
高安全要求 JWT + Redis黑名单 可控失效机制
大型SSO JWT + 刷新机制 标准、可扩展

六、尾声:记忆与契约的两种浪漫

Session 像个老和尚——稳重、可靠,凡事记在心头;
JWT 像个浪子——自由、签约、无拘无束。

真正的高手不是选谁,而是清楚:

  • 凭证放哪?
  • 状态存哪?
  • 何时让它失效?

HTTP 或许无情,但程序员让它有了“记忆”。
因为被记住,本身就是一种浪漫。