Java程序员的鉴权江湖:Session 与 JWT 的爱恨情仇

64 阅读4分钟

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


一、鉴权的核心哲学

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

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

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

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

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

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

二、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(默认)浏览器自动携带简洁,安全性高,可加 HttpOnlySecure
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:同源不同命

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

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


五、实战选型建议

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

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

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

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

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

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