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 在内存中到底怎么存的?

Session 的本质就是服务器内存里一个 Map<SessionID, 数据>。

以 Tomcat 为例,它内部维护了一个 ConcurrentHashMap:

1
2
3
4
5
6
7
8
9
┌──────────────────────────────────────────────────────┐
│ Tomcat 内存(ManagerBase) │
│ │
│ Map<String, Session> sessions = { │
│ "ABC123" → { user: "neo", role: "admin" }, │
│ "DEF456" → { user: "trinity", role: "user" }, │
│ "GHI789" → { user: "morpheus", role: "admin" } │
│ } │
└──────────────────────────────────────────────────────┘

完整交互流程:

1
2
3
4
5
1. 用户登录 → 服务器生成随机 SessionID(如 "ABC123")
2. 以 SessionID 为 key,用户数据为 value,存入内存 Map
3. 通过 Set-Cookie: JSESSIONID=ABC123 返回给浏览器
4. 后续请求浏览器自动带上 Cookie: JSESSIONID=ABC123
5. 服务器拿到 ID,从 Map 中 get("ABC123") 取出用户数据

代价很明确:每个在线用户都占用服务器内存。
1万人同时在线 = 1万份 Session 对象常驻内存。
服务器重启 = 内存清空 = 全部用户登录状态丢失。


⚙️ 分布式 Session:共享记忆

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

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

解决方案:

方案 思路 优点 缺点
Session复制 节点间广播同步Session 应用透明 节点越多同步越慢
粘性会话 Nginx按IP/Cookie固定路由到同一节点 实现最简单 节点挂了会话就丢
Redis 集中存储 Session统一存到Redis 快、支持过期 依赖Redis
JDBC 数据库存储 Session存到MySQL等关系库 无额外中间件 性能差,高并发扛不住
Hazelcast Session存到分布式内存网格 去中心化、无单点 运维复杂、网络分区敏感

为什么大多数团队选 Redis?
读写微秒级、天然支持 TTL 自动过期、运维生态成熟。
但如果项目小、不想多引一个中间件,JDBC 方案也能撑住——只是别在高并发场景下用。


方案一:Redis 共享(主流推荐)

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}

方案二:JDBC 数据库存储(不想引入 Redis 时的备选)

1
2
3
4
5

<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-jdbc</artifactId>
</dependency>
1
2
3
spring:
session:
store-type: jdbc

Spring Session 会自动创建 SPRING_SESSION 表:

1
2
3
SESSION_ID
| PRINCIPAL_NAME | CREATION_TIME | LAST_ACCESS_TIME | ATTRIBUTE_BYTES
ABC123 | neo | 1736971200000 | 1736974800000 | (序列化数据)

代价:每次请求都要查数据库,相当于把 Map.get(sessionId) 变成了一次 SQL 查询。
适合内部管理系统、并发量不高的场景。


不一定。

方式 说明 优点
Cookie(默认) 浏览器自动携带 简洁,安全性高,可加 HttpOnly、Secure
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 或许无情,但程序员让它有了“记忆”。
因为被记住,本身就是一种浪漫。