Spring Security基础:快速搭建安全认证系统的实战经验分享

林娜
2025-06-17 13:58
阅读 770

大家好,我是有着五年后端开发经验的一线工程师。从最初的Spring Boot初学者到如今负责一个中大型项目的架构设计,Spring Security一直是我在构建系统安全方面的重要工具。今天想和大家分享一下我在这个过程中的一些经验,特别是围绕“如何快速搭建一个安全认证系统”这个主题。

这篇文章不会堆砌一堆理论知识,而是会结合我自己的项目经历,带你看清实际落地的Spring Security应用到底是怎么跑起来的。尤其是当你面对的是一个需要在短时间内上线的安全系统时,这套框架能帮你解决很多问题。

背景介绍:为什么选择Spring Security?

API接口文档-1

背景介绍:为什么选择Spring Security?

记得去年我参与了一个公司内部的服务平台重构项目。这个平台原本是基于传统的Servlet+Filter模式实现的权限控制,但随着用户量的增长和业务逻辑的复杂化,老系统在安全性和扩展性上都暴露出很多问题。比如:

  • 用户登录信息容易被伪造;
  • 权限分配粒度粗,难以管理;
  • 没有统一的接口鉴权机制,导致某些API可以绕过登录访问;
  • 登录后的 Session 保存方式不统一,跨服务间共享困难;
  • 系统维护成本高,每次改权限都需要手动改代码、重启服务。

当时我们决定重构整个身份认证模块,并引入更现代化的安全框架来支撑后续的微服务拆分。考虑到团队的技术栈主要是 Java + Spring Boot,所以自然把目光锁定在 Spring Security 这个官方推荐的安全框架上。

遇到的挑战:不是所有文档都能解决问题

遇到的挑战:不是所有文档都能解决问题

虽然 Spring Security 官方文档很详尽,但真正用起来却发现文档和实践之间还是有不小的差距。尤其是在以下这些方面遇到了不少坑:

  1. 多种安全策略并存的情况下怎么配置?
  2. 如何优雅地集成 JWT 做无状态认证?
  3. 自定义的 UserDetailsService 和 UserDetails 实现总出错?
  4. CSRF、CORS、Session管理等概念理解不清时容易配崩。
  5. 如何与数据库交互完成真正的用户认证?

特别是在早期版本中,Spring Security 的 API 变动频繁,有时候按照网上的教程写完之后发现根本不起作用,还要反复 debug 才知道是新旧版本的写法变了。

这些问题让我意识到,光看文档远远不够,必须深入理解其背后的原理和流程,才能在不同场景中灵活使用。

解决方案:一步步搭建安全认证核心模块

解决方案:一步步搭建安全认证核心模块

整个系统的安全模块目标很明确:支持多角色权限体系,实现基于 Token(JWT)的无状态鉴权,同时兼顾传统 Cookie Session 的兼容支持,最终做到可插拔、可扩展。

技术选型和架构设计

我们在安全模块的设计上采用了如下技术组合:

  • Spring Boot 2.7 + Spring Security 5.7:选择相对稳定的新版本,支持 OIDC、OAuth2 和较完整的 JWT 支持。
  • JWT + Redis:用于 Token 的生成与存储,Redis 缓存用户的在线状态,避免频繁访问 DB。
  • MySQL 数据库存储用户信息、角色、权限表:采用关系型结构,便于维护。
  • MyBatis Plus + Druid:操作数据库,配合 Druid 监控 SQL 性能。
  • Logback + MDC 日志追踪:记录安全相关操作日志,方便后期审计。

整体架构图简化如下:

[客户端] --> [Nginx/网关层] --> [业务模块]
                   |
             [安全认证中心]
            /      |        \
     [JWT签发] [Session管理] [权限校验]

其中的安全认证中心负责处理所有身份验证、授权和令牌发放的工作。

核心流程梳理

为了让大家有个清晰的认知,这里简单画了一个用户登录流程的时序图:

  1. 用户通过 /login 接口提交账号密码;
  2. 后端使用 AuthenticationManager.authenticate(...) 去验证;
  3. 自定义的 UserDetailsService 从数据库加载用户数据;
  4. 密码匹配成功后,生成 JWT token 并返回给前端;
  5. 前端将 token 放入请求头(Authorization: Bearer xxx),后续请求携带该 token;
  6. 拦截器通过解析 JWT 获取用户信息,交给 Spring Security 上下文,完成鉴权;
  7. 访问受保护的资源(如 /api/admin/user-list)时由 @PreAuthorize("hasRole('ADMIN')") 等注解拦截并校验权限。

接下来我们会看看这一套流程是怎么通过 Spring Security 快速搭出来的。

代码实践:关键组件实现思路详解

下面我会展示几个关键部分的实现,包括配置类、自定义过滤器、Token 工具类等。

1. 安全配置主类

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig {

    private final JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
    private final AccessDeniedHandler accessDeniedHandler;
    private final AuthenticationEntryPoint authenticationEntryPoint;

    public SecurityConfig(JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter,
                          AccessDeniedHandler accessDeniedHandler,
                          AuthenticationEntryPoint authenticationEntryPoint) {
        this.jwtAuthenticationTokenFilter = jwtAuthenticationTokenFilter;
        this.accessDeniedHandler = accessDeniedHandler;
        this.authenticationEntryPoint = authenticationEntryPoint;
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
                .csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)
                .exceptionHandling()
                .accessDeniedHandler(accessDeniedHandler)
                .authenticationEntryPoint(authenticationEntryPoint)
                .and()
                .authorizeRequests()
                .antMatchers("/login").permitAll()
                .anyRequest().authenticated()
                .and()
                .build();
    }

    // 其他 Bean 如 passwordEncoder、AuthenticationManager 略
}

这段配置干了这么几件事:

  • 关闭 CSRF:因为我们使用的是 JWT,不需要防范 CSRF;
  • 设置为无状态 Session:不再依赖容器的 Session;
  • 添加自定义 JWT 过滤器
  • 处理异常情况:没有权限访问或未登录的情况,分别由两个处理器处理;
  • 设置访问控制规则

📌 小提示:如果你的应用前后端同源部署,可能仍需开启 CORS 或者适当调整配置,防止跨域请求被安全机制拦下。

2. 自定义 JWT 过滤器

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    private final JwtService jwtService;
    private final UserDetailsServiceImpl userDetailsService;

    public JwtAuthenticationTokenFilter(JwtService jwtService, UserDetailsServiceImpl userDetailsService) {
        this.jwtService = jwtService;
        this.userDetailsService = userDetailsService;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException {
        String token = getTokenFromRequest(request);
        if (token != null && jwtService.validateToken(token)) {
            String username = jwtService.getUsernameFromToken(token);
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);

            UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
                    userDetails, null, userDetails.getAuthorities()
            );
            authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        chain.doFilter(request, response);
    }

    private String getTokenFromRequest(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }
}

这一步是最关键的安全拦截点。它会:

  • 从 Header 中提取 Token;
  • 验证 Token 是否有效;
  • 如果有效,则去数据库加载用户详情;
  • 创建 Authentication 对象,并塞进当前线程上下文中,供后续鉴权使用。

💡 个人建议:不要在这里做数据库查询太多次,可以在 JwtService 中缓存一部分用户信息,减少压力。

3. 使用 @PreAuthorize 控制接口权限

一旦你配置好了 @EnableGlobalMethodSecurity(prePostEnabled = true),就可以在 Controller 上直接使用如下注解:

@GetMapping("/users")
@PreAuthorize("hasAuthority('user:read')")
public List<User> getAllUsers() {
    return userService.findAll();
}

这种方式非常直观,也符合实际的权限设计需求。当然,你也可以使用 hasRole() 来判断用户是否具备某个角色权限。

不过有一点需要注意:Spring Security 默认的权限前缀是 ROLE_,如果你在数据库里存的角色名没有带上这个前缀,比如叫 "ADMIN",那么你要么加一个自定义的 GrantedAuthority,要么使用如下形式:

@PreAuthorize("hasRole('ADMIN')")

然后确保你的 UserDetails 返回的 authorities 是带有 ROLE_ 前缀的。

4. 登录接口实现

登录接口的核心在于调用 Spring Security 的 AuthenticationManager.authenticate(...) 方法:

@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest loginRequest) {
    Authentication authentication = authenticationManager.authenticate(
            new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword())
    );

    SecurityContextHolder.getContext().setAuthentication(authentication);

    String token = jwtService.generateToken(authentication);
    return ResponseEntity.ok().header("Authorization", "Bearer " + token).build();
}

这段代码背后触发了几个关键流程:

  1. Spring Security 内部找到匹配的 AuthenticationProvider,一般是 DaoAuthenticationProvider
  2. 提取用户名密码,调用 UserDetailsService 加载用户;
  3. 匹配输入密码与数据库中的加密后的值(通常使用 BCryptPasswordEncoder);
  4. 如果通过认证,则构建一个经过身份验证的 Authentication 实例;
  5. 最后我们拿着这个实例生成 JWT Token 返回。

⚠️ 注意点:默认情况下,如果认证失败,Spring Security 会抛出异常,而不是返回 JSON 错误。因此你需要统一捕获 AuthenticationException 并封装成 JSON Response 返回。

踩坑经验分享:那些让你夜不能寐的问题

缓存策略对比-2

下面我想分享几个在实际工作中遇到的真实问题及解决方案:

1. CORS 与 OPTIONS 请求被拦截

我们初期在前端使用 Vue 调用接口的时候,总是出现预检请求失败的问题。后来发现是因为没有放行 OPTIONS 请求,而且 Spring Security 的顺序配置不当也会造成问题。

解决方案

http.authorizeRequests()
    .antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
    ...

此外,还可以在全局配置中显式允许跨域:

@Bean
public CorsFilter corsFilter() {
    CorsConfiguration config = new CorsConfiguration();
    config.addAllowedOrigin("*");
    config.addAllowedHeader("*");
    config.addAllowedMethod("*");

    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", config);
    return new CorsFilter(source);
}

再将其添加到 Spring Security 的过滤链前面:

.addFilterBefore(corsFilter(), WebAsyncManagerIntegrationFilter.class)

这样就解决了跨域问题。

2. JWT 被篡改但仍然通过验证

有一次测试环境发现,有些非法 Token 竟然也能通过验证!后来排查发现是因为签名密钥太简单或者没正确传递。

建议

  • 使用长度足够的随机字符串作为密钥;
  • 在生成和验证时保持一致;
  • 不要硬编码密钥,可以通过配置文件注入;
  • 推荐使用 io.jsonwebtoken 提供的库来做 JWT 的签发与校验。

3. 多种认证方式混用时搞乱过滤链顺序

一开始我尝试在一个系统中同时支持 JWT 与 Session 登录,结果两种方式互相干扰。

后来通过将两者分离为不同的子路径(比如 /api/v1/auth/* 下使用 JWT,其他路径使用 Session 登录),并通过多个 SecurityFilterChain 分别配置才解决了这个问题。

@Bean
@Order(1)
public SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exception {
    return http
            .securityMatcher("/api/v1/**")
            ...
            .build();
}

@Bean
@Order(2)
public SecurityFilterChain webSecurityFilterChain(HttpSecurity http) throws Exception {
    return http
            .formLogin()
            ...
            .build();
}

这种结构让不同路径走不通的安全链路成为可能。

效果总结:系统上线后的收益

这套基于 Spring Security 的安全认证系统上线后,我们的系统在以下几个方面得到了显著提升:

  • 安全性提升:统一的身份认证机制,防止越权访问;
  • 开发效率提高:权限控制变得可视化,修改只需配置注解;
  • 接口响应速度变快:避免了重复的权限校验逻辑;
  • 运维更方便:权限变动无需重新部署服务;
  • 扩展性强:未来接入 OAuth2、SSO 等更加容易。

更重要的是,系统上线后几乎没有因权限问题引发的重大线上故障。这说明这套方案不仅满足了功能需求,也具备良好的稳定性和健壮性。

经验总结与建议

通过这一次对 Spring Security 的深入实践,我也积累了一些心得体会,在这里分享给大家:

✅ 推荐的做法:

  • 使用 @PreAuthorize 做方法级权限控制,比在业务层硬编码更清晰;
  • 使用 JWT 做无状态认证,适合 RESTful API 场景;
  • 将权限分为 ROLE + AUTHORITY 两级,方便管理;
  • 引入 Redis 缓存在线用户列表,实现强制登出;
  • 结合日志系统跟踪安全事件,方便后期审计。

❌ 应该避免的地方:

  • 不要在拦截器或过滤器中频繁访问数据库;
  • 避免在 Token 中存放敏感信息;
  • 不要忽视 Session 的生命周期管理;
  • 避免混合使用多种安全机制而不划分路径;
  • 不要随意关闭 CSRF,除非你真的不需要。

👨‍💻 给新手的一点建议:

如果你是刚开始接触 Spring Security 的同学,我的建议是:

  1. 先理解整个流程图,比如 Authentication 是怎么一步一步走到最后的;
  2. 自己动手写一个最小可运行的安全工程,哪怕是只保护一个接口;
  3. 遇到问题不要怕折腾,调试是最好的老师;
  4. 结合日志去看流程,很多时候问题就是出现在某一个 filter 没被触发;
  5. 多查 Spring Security 的 issue 列表或社区讨论,说不定别人早就踩过同样的坑。

写在最后:技术的本质是为了创造价值

其实这篇文章写到这里,我已经不止一次回想起那段时间和队友们一起调试、修 Bug、查日志的日子。虽然过程有点煎熬,但当我看到系统稳定上线、权限逻辑井井有条时,心里那种踏实感是真实的。

Spring Security 不只是一个框架,它更像是我们守护系统安全的一道坚固防线。而作为一名后端工程师,能够熟练掌握这门技术,就意味着你能为团队带来更高的安全保障和开发效率。

希望这篇来自真实工作场景的经验分享,对你有所帮助。也欢迎你在评论区交流你在实际项目中遇到的安全问题,我们可以一起探讨更好的解决方案。


如果你觉得这篇文章对你有用,欢迎点赞、收藏、转发。也欢迎关注我的公众号【Java成长日记】,一起交流后端技术的成长之路。

评论 0

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝