Spring Security认证服务器写错一次,线上炸了3小时?别让“默认配置”害了你!

你是不是也遇到过:本地跑得好好的Spring Security认证服务器,一上生产就疯狂返回401、token无效、refresh_token被拒?
你照着官方文档抄的代码,配了JWT、写了UserDetailsService、加了@EnableWebSecurity——一切看起来都对,可就是“不认人”。
你查日志、翻源码、重启十遍,最后发现:不是代码错了,是你把“默认行为”当成了“预期行为”

今天,我就带你扒一扒那些藏在Spring Security认证服务器里、比“@Transactional失效”还隐蔽的致命陷阱。
别再让“默认值”背锅了——这些坑,踩过一次,够你写半年周报。


原理浅析:认证流程到底是谁在“验身”?

很多人以为加个@EnableWebSecurity就万事大吉,但Spring Security的认证流程,其实是一场精密的“接力赛”,中间任何一个环节脱节,用户就会被“保安拦在门外”。

我们先看一个典型的JWT认证流程:

Client FilterChain JwtAuthenticationFilter AuthenticationManager UserDetailsService TokenProvider 发送Authorization: Bearer <token> 拦截请求 解析并校验token签名 根据token中uid加载UserDetails 返回Authenticated User 提交Authentication对象 认证成功,设置SecurityContext 放行,返回业务数据 抛出BadCredentialsException 设置Authentication为null 返回401 Unauthorized alt [token有效] [token无效/过期/篡改] Client FilterChain JwtAuthenticationFilter AuthenticationManager UserDetailsService TokenProvider

关键点来了

  • 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应一次性使用,用后即废,并记录使用记录
未配置异常处理器 ⚠️ 中危 自定义AuthenticationEntryPointAccessDeniedHandler,统一返回JSON格式错误

最后一句掏心窝子的话

Spring Security不是“开箱即用”的玩具,它是需要你亲手拧紧每一颗螺丝的精密仪器。
它的“默认配置”不是为了让你快速上线,而是为了让你在安全和便利之间,做出清醒的选择

你写的每一段SecurityFilterChain,都在决定谁可以进入你的系统。
别让“我以为”成为你凌晨三点的报警电话。

下次再有人问你:“为什么我的认证服务器这么不稳定?”
你可以微笑着回答:
“因为我从不信任默认值。”

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐