HTTP 是健忘的,而程序员是执着的。 我们为让服务器记住用户,造出了两种记忆体系:Session——稳重老派;JWT——灵动新潮。 一个靠“查档案”,一个靠“签契约”。
一、鉴权的核心哲学
HTTP 是无状态的,每一次请求都是陌生人上门:
“我是谁?你凭什么信我?”
于是程序员发明了两种记忆机制:
| 流派 | 思想核心 |
|---|---|
| Session 派 | 服务端保存状态:我记得你是谁。 |
| JWT 派 | 客户端自带凭证:你自己证明你是谁。 |
无论选哪派,核心过程都一样:
- 用户登录成功,获得凭证。
- 每次请求带上凭证。
- 服务端验证凭证是否可信。
二、Session:服务器的温柔记忆
🌱 单机版原理与代码
// 登录接口示例
@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()。 - 服务器重启或内存清除。
配置示例:
server.servlet.session.timeout: 60m
⚙️ 分布式 Session:共享记忆
单机没问题,但一旦负载均衡——
用户第一次访问 -> Server A(创建Session)
第二次访问 -> Server B(查不到Session)
解决方案三选一:
| 方案 | 思路 | 优点 | 缺点 |
|---|---|---|---|
| Session复制 | 节点同步Session | 应用透明 | 同步代价大 |
| 粘性会话 | 用户固定访问同一节点 | 实现简单 | 容灾差 |
| Session共享(推荐) | 使用Redis集中存储Session | 稳定可扩展 | 有外部依赖 |
Redis共享Session示例:
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
spring:
session:
store-type: redis
redis:
host: localhost
port: 6379
Redis 实际存储内容:
spring:session:sessions:abc123 -> {"user":"neo", "expireTime":1736971200000}
❓Session 必须放在 Cookie 吗?
不一定。
| 方式 | 说明 | 优点 |
|---|---|---|
| Cookie(默认) | 浏览器自动携带 | 简洁,安全性高,可加 HttpOnly、Secure |
| Header | 手动在请求头中传递 Authorization: Session abc123 | 适合跨域和移动端 |
| URL 参数 | ?sid=abc123 | ⚠️ 极不安全,禁止用于生产 |
✅ 结论:SessionID 只是凭证,可以放任何地方,只要服务端能取到即可。
Cookie 不是必须的,只是“最懂浏览器的信使”。
三、JWT:客户端的自由契约
JWT(JSON Web Token)主张:
“状态我自己带,信任靠签名。”
🧩 结构拆解
JWT由三部分组成:
Header.Payload.Signature
-
Header:说明算法与类型,例如:
{"alg": "HS256", "typ": "JWT"} -
Payload:承载用户数据:
{"sub": "neo", "role": "admin", "exp": 1760978291} -
Signature:签名,防止被篡改。
生成Token示例:
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:
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,防止重放攻击。
黑名单验证示例:
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 或许无情,但程序员让它有了“记忆”。 因为被记住,本身就是一种浪漫。