三年前,每当我有人问及Spring Boot中的身份验证时,我都会给出同样的答案:“使用JWT,它是无状态的,它可以扩展。”我一半是对的,一半是错的,直到我继承了两个生产代码库——其中一个以一种非常具体的方式损坏了——我才明白哪一半是对的,哪一半是错的。
这不是关于如何实现其中任何一个的教程。这是我希望在我开始默认推荐JWT之前就拥有的决策指南。
真正教你什么教程
大多数 Spring Boot 安全教程会带你了解 JWT,因为它能让演示更简洁。你添加一个过滤器,验证一个签名,设置 SecurityContext,搞定。没有数据库调用,没有共享状态,天生无状态。感觉架构上很清晰。
他们很少展示:当用户更改密码时会发生什么。或者他们的账户被暂停。或者在一个设备上登出,并期望这意味着所有设备都会发生什么。在一个纯 JWT 设置且没有黑名单的情况下,这三个问题的答案是“直到令牌过期什么都不会发生。”
另一方面,会话感觉过时了。“那不扩展。” “你需要粘性会话。” 这两者都不再正确,我会向你展示为什么。
Spring Boot 中会话的实际工作原理
Spring Session with Redis 只需要三个注解和一个依赖:
@EnableRedisHttpSession
@Configuration
public class SessionConfig {
// Spring Session handles everything else
}
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
客户端获取一个不可见的会话 ID(通常是 32 个十六进制字符),该 ID 存储在HttpOnly; Secure cookie。每个请求都会发送这个 cookie,Spring 在 Redis 中查找会话,反序列化它,并填充到 SecurityContext 中。会话数据存储在 Redis 中,而不是 token 本身.
撤销是 sessionRepository.deleteById(sessionId)。即时生效,没有例外.
水平扩展开箱即用——每个实例都连接到同一个 Redis。不需要粘性会话。这已经不是 2009 年了。
JWT:你实际获得的内容
JWT 是一个经过 base64 编码的 JSON 有效载荷(声明),使用密钥或私钥进行签名。服务器不会存储它。通过检查签名进行本地验证——无需数据库调用,无需网络跳转。
这在两种特定情况下很重要:
独立验证令牌的微服务。 如果你拥有五个服务并且每个服务都需要知道调用者是谁,那么会话就需要每个服务调用共享存储或中央认证服务。JWT 允许每个服务仅使用公钥或共享密钥在本地验证令牌。
第三方和移动客户端。 浏览器自动处理 Cookie。原生应用和第三方 API 客户端则不处理。JWT 在Authorization 标头无需cookie配置即可在任何地方工作.
在这两种情况之外,你支付了JWT的成本,却没有获得JWT的好处.
这些成本是真实的:
- 没有黑名单就无法撤销。 15分钟的访问令牌不能提前失效。被盗的令牌在过期之前一直有效。如果你在每次请求中都添加一个 Redis 阻止列表进行检查,你就只是重新添加了你试图避免的数据库调用。
- 令牌大小。 会话 Cookie 是 32 字节。一个包含少量声明的 JWT 在每次请求头中占用 300–600 字节,永远如此。在高频内部 API 中,这会累积起来。
-
实现表面. JWT有着长期的安全漏洞历史:
alg: none攻击、弱的HMAC密钥、缺少过期验证、错误的观众检查。Spring Security正确处理了大部分这些问题,但复杂性预算高于会话.
真正的决策框架
停止询问"哪个更好?"并询问"我实际上需要什么?"
使用会话时:
- 您控制前端——一个基于浏览器的应用程序,使用您自己的后端
- 您需要立即撤销:注销意味着注销,密码更改意味着所有会话都失效
- 您正在构建一个单体应用程序或一个小型服务,该服务拥有自己的身份验证
- 您已经使用 Redis 进行缓存或队列操作
使用 JWT 时:
- 多个独立服务需要在不调用中央存储的情况下验证身份
- 您有非浏览器客户端——移动应用、命令行工具、第三方集成——这些客户端难以处理 Cookie
- 您需要联合身份验证:由外部身份提供者(Auth0、Keycloak、Cognito)发布的令牌,您的服务进行验证
不要使用 JWT,因为:
- "它是无状态的,并且可以扩展" — Redis会话在任何数量的实例之间扩展得一样好
- "每个人都用它" — 盲目模仿安全决策就是你会得到在生产环境中不可撤销的7天令牌的原因
谁都不谈论的混合设置
我所见过的许多生产系统在这方面的优秀实践,都是结合使用:浏览器前端使用会话,服务间调用和移动客户端使用 JWT。
Spring Security 通过多个 SecurityFilterChain 组件干净地支持这一点:
@Bean
@Order(2)
public SecurityFilterChain jwtFilterChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/api/v1/**")
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.csrf(AbstractHttpConfigurer::disable)
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated());
return http.build();
}
@Bean
@Order(1)
public SecurityFilterChain sessionFilterChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/web/**")
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED))
.formLogin(Customizer.withDefaults())
.logout(logout -> logout.logoutSuccessUrl("/web/login?logout"))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/web/login").permitAll()
.anyRequest().authenticated());
return http.build();
}
两条链,不同的匹配器,不同的策略。浏览器应用获取会话和完整撤销。API和服务调用获取JWT。Spring按@Order顺序应用它们——第一个匹配的链获胜.
实际的性能差异看起来是怎样的
会话每次请求增加一个 Redis 循环往返时间——在配置良好的本地 Redis 上,通常为 0.5–2ms,如果 Redis 位于单独的可用区,则为 2–5ms。对于处理时间已经需要 50–200ms 的请求来说,这是微不足道的。
JWT 验证是在内存中:解析 base64,验证 HMAC 签名,检查过期时间。亚毫秒级。如果你正在构建一个需要每秒处理数千次请求且预算为微秒级的 API,这种差异很重要。如果你正在构建一个标准的 Web 应用程序或业务 API,则没有关系.
令牌大小的差异在总体上比你预期的更重要。会话 Cookie 是SESSION=<32-hex-chars> — 在 Cookie 头部大约有 50 字节。JWT 是 Authorization: Bearer <base64> — 在每个请求中,Authorization 头部通常为 400–700 字节。在一个有 10,000 个活跃用户、每小时每个用户发起 20 次请求的应用中,仅头部开销就大约是会话的 14 倍(会话为 5MB,头部为 70MB/小时)。在内部高频调用的微服务 API 中,这会累积成实际成本。
这两者本身都不是选择其中一方的理由。它们是需要与架构适配性权衡的因素,而不是让你做决定的论据。
JWT 容易犯的安全错误
如果你正确使用 Spring Security,它可以帮助你避免许多 JWT 的陷阱,但我见过所有这些在生产代码库中:
对称密钥太短。 HS256 需要 256 位(32 字节)的密钥。短密钥可以通过暴力破解。使用 openssl rand -base64 32 生成它,并将其存储在你的密钥管理器中,而不是 application.yml 中。
不进行观众或发行者验证。 如果你有多個服务接受相同的 JWT,为服务 A 签发的令牌可能会被重放攻击服务 B,除非你验证 aud。iss 声称。Spring Security 的 JwtDecoder 支持 .claimValidator("aud", ...)。
记录令牌。 访问日志、调试语句、错误跟踪。JWT 是一种凭证。在您的日志配置中将其视为密码。
在单体应用中使用 RS256。 RS256(非对称)在多个服务需要验证单一认证服务发布的令牌时是有意义的。在一个单体应用中,只有一个服务负责发行和验证令牌,RS256会增加密钥管理的复杂性,而没有任何安全优势。HS256是正确的默认选项.
我最常看到的错误
团队最初使用JWT是因为一个教程推荐了它。六个月后,他们需要实现“从所有设备登出”或“密码更改后强制重新认证”。在那个时刻,他们添加了一个Redis黑名单来在过期前使令牌失效——这意味着现在每个JWT验证都会在每个请求中访问Redis。他们保留了所有JWT的复杂性,并在顶部添加了会话存储。
我已经做完了。结果代码比纯会话或纯JWT更难理解。
如果你需要撤销功能,请使用会话。如果你确实需要无状态的跨服务验证,请使用JWT,并有意设计撤销限制——15分钟的访问令牌,刷新令牌轮换,在发货前制定明确的会话失效策略——而不是六个月后当作事后想法。
无聊的答案往往是对的。Spring Session with Redis 自 2015 年起就正确地解决了这个问题。JWT 解决了一个特定的分布式系统问题。在做出选择之前,先了解你实际上面临的是哪种问题。
你当前的设置使用的是什么,这是一个深思熟虑的选择还是来自教程?
最初发布于 Medium。

























