Spring Security认证服务器8大致命坑:别再让默认配置害你线上炸3小时
你是不是也以为加个@EnableWebSecurity就能高枕无忧?结果生产环境token全失效,凌晨三点被报警电话叫醒?别怪Spring Security太狠——是你把“默认配置”当成了“默认安全”。本文血泪拆解JWT认证服务器三大致命坑:过滤器没注册、权限丢了、过期token照用不误!用Mermaid流程图扒透认证接力赛,代码对比直击痛点,教你显式注入Filter、正确返回Authoritie
Spring Security认证服务器写错一次,线上炸了3小时?别让“默认配置”害了你!
你是不是也遇到过:本地跑得好好的Spring Security认证服务器,一上生产就疯狂返回401、token无效、refresh_token被拒?
你照着官方文档抄的代码,配了JWT、写了UserDetailsService、加了@EnableWebSecurity——一切看起来都对,可就是“不认人”。
你查日志、翻源码、重启十遍,最后发现:不是代码错了,是你把“默认行为”当成了“预期行为”。
今天,我就带你扒一扒那些藏在Spring Security认证服务器里、比“@Transactional失效”还隐蔽的致命陷阱。
别再让“默认值”背锅了——这些坑,踩过一次,够你写半年周报。
原理浅析:认证流程到底是谁在“验身”?
很多人以为加个@EnableWebSecurity就万事大吉,但Spring Security的认证流程,其实是一场精密的“接力赛”,中间任何一个环节脱节,用户就会被“保安拦在门外”。
我们先看一个典型的JWT认证流程:
关键点来了:
JwtAuthenticationFilter是无状态认证的核心,它不依赖Session,完全靠token解析。- 但这个Filter默认不会自动注册!你必须显式配置它插入过滤器链。
AuthenticationManager默认使用ProviderManager,它会遍历所有AuthenticationProvider,其中DaoAuthenticationProvider依赖你的UserDetailsService。- 如果你没重写
configure(AuthenticationManagerBuilder),Spring Security会用一个空的、什么都不认的默认Provider!
你以为你在用JWT,实际上——系统压根没在验证你传的token,它在验证一个“幽灵用户”。
八大坑点代码实录:从“能跑”到“能活”
❌ 坑1:忘了注册JWT过滤器 —— “我传了token,为啥没人看?”
错误写法(你以为的“标准配置”):
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
// ✅ 你只写了上面这些,然后就以为JWT生效了?
return http.build();
}
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() {
return new JwtAuthenticationFilter(); // 这个bean创建了,但没人用!
}
}
问题在哪?
你创建了JwtAuthenticationFilter,但它没有被添加到过滤器链中!Spring Security默认只加载内置的UsernamePasswordAuthenticationFilter等,你的自定义Filter形同虚设。
正确写法:
@Bean
public SecurityFilterChain filterChain(HttpSecurity http, JwtAuthenticationFilter jwtAuthFilter) throws Exception {
http
.csrf().disable()
.authorizeHttpRequests(auth -> auth
.requestMatchers("/auth/**").permitAll() // 登录接口放行
.anyRequest().authenticated()
)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class); // ✅ 关键!插队进去
return http.build();
}
📌 记住:
addFilterBefore()或addFilterAfter()是唯一能把自定义Filter注入链中的方式。
别指望@Component+@Order能救你——那只是给Bean排序,不是给过滤器链排序!
❌ 坑2:UserDetailsService返回的UserDetails没实现getAuthorities() —— “我明明有ROLE_ADMIN,怎么还是403?”
错误写法:
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("用户不存在"));
// ❌ 只设置了用户名和密码,权限全丢了!
return new org.springframework.security.core.userdetails.User(
user.getUsername(),
user.getPassword(),
Collections.emptyList() // 啊?权限呢?!
);
}
}
后果:
即使你传的是合法token,AuthenticationManager通过DaoAuthenticationProvider验证密码后,发现UserDetails.getAuthorities()为空 → 认证成功,但无任何权限 → 后续访问任何受保护接口直接403。
正确写法:
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("用户不存在"));
// ✅ 从数据库加载角色,转换为GrantedAuthority
List<GrantedAuthority> authorities = user.getRoles().stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role.getName()))
.collect(Collectors.toList());
return new org.springframework.security.core.userdetails.User(
user.getUsername(),
user.getPassword(),
authorities // ✅ 权限在这里!
);
}
💡 更优雅的做法:自定义
UserDetails实现类,把User实体与UserDetails解耦,避免硬编码ROLE_前缀。
public class CustomUserDetails implements UserDetails {
private final User user;
public CustomUserDetails(User user) {
this.user = user;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return user.getRoles().stream()
.map(r -> (GrantedAuthority) () -> r.getName()) // 直接用数据库字段名,如 "ADMIN", "USER"
.collect(Collectors.toList());
}
// ... 其他方法省略
}
这样你就能在数据库存"ADMIN"而不是"ROLE_ADMIN",更符合现代微服务规范。
❌ 坑3:JWT Token过期后,refresh_token没做二次校验 —— “用户登出后还能用旧token继续调用?”
错误写法:
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private TokenProvider tokenProvider;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String token = extractToken(request);
if (token != null && tokenProvider.validateToken(token)) { // ✅ 只校验签名,不校验是否过期?
Authentication auth = tokenProvider.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(auth);
}
filterChain.doFilter(request, response);
}
}
问题:validateToken()如果只校验签名,而不检查exp(过期时间),那么即使token已过期,只要没被篡改,依然能通过!
这等于说:你家门锁坏了,但防盗门还在,小偷拿原钥匙照样进。
正确写法:
public class TokenProvider {
public Authentication getAuthentication(String token) {
Claims claims = Jwts.parser()
.verifyWith(secretKey)
.build()
.parseSignedClaims(token)
.getPayload();
String username = claims.getSubject();
List<GrantedAuthority> authorities = ((List<?>) claims.get("authorities"))
.stream()
.map(o -> new SimpleGrantedAuthority((String) o))
.collect(Collectors.toList());
// ✅ 必须检查过期时间!否则等于没认证
Date expiration = claims.getExpiration();
if (expiration.before(new Date())) {
throw new ExpiredJwtException(null, null, "Token已过期");
}
return new UsernamePasswordAuthenticationToken(username, null, authorities);
}
public boolean validateToken(String token) {
try {
Jwts.parser()
.verifyWith(secretKey)
.build()
.parseSignedClaims(token);
return true;
} catch (ExpiredJwtException e) {
log.warn("Token已过期: {}", e.getMessage());
return false; // ✅ 明确拒绝过期token
} catch (Exception e) {
return false;
}
}
}
🔥 高阶技巧:结合Redis维护黑名单(logout时将token加入blacklist),防止token被窃取后仍可使用。
总结:认证服务器避坑指南(开发者血泪清单)
| 坑点 | 危险等级 | 正确做法 |
|---|---|---|
| 未注册自定义Filter | ⚠️ 致命 | 使用 .addFilterBefore() 显式插入过滤器链 |
| UserDetails缺失权限 | ⚠️ 致命 | 确保getAuthorities()返回非空集合,且映射正确 |
| 忽略Token过期校验 | ⚠️ 高危 | 所有JWT解析必须校验exp字段,拒绝过期token |
| 使用默认AuthenticationManager | ⚠️ 中危 | 显式配置AuthenticationManagerBuilder,绑定UserDetailsService |
| token存储在localStorage无防护 | ⚠️ 中危 | 考虑HttpOnly + Secure Cookie,或配合CSRF Token |
| 未处理Refresh Token轮换 | ⚠️ 中危 | refresh_token应一次性使用,用后即废,并记录使用记录 |
| 未配置异常处理器 | ⚠️ 中危 | 自定义AuthenticationEntryPoint和AccessDeniedHandler,统一返回JSON格式错误 |
最后一句掏心窝子的话
Spring Security不是“开箱即用”的玩具,它是需要你亲手拧紧每一颗螺丝的精密仪器。
它的“默认配置”不是为了让你快速上线,而是为了让你在安全和便利之间,做出清醒的选择。
你写的每一段SecurityFilterChain,都在决定谁可以进入你的系统。
别让“我以为”成为你凌晨三点的报警电话。
下次再有人问你:“为什么我的认证服务器这么不稳定?”
你可以微笑着回答:
“因为我从不信任默认值。”
更多推荐


所有评论(0)