从零构建安全认证系统:我在项目中踩过的Spring Security坑

孙明
2025-06-26 07:49
阅读 503

背景介绍:为什么需要一个可靠的安全框架?

背景介绍:为什么需要一个可靠的安全框架?

我目前在一家互联网公司负责用户中心模块的开发,产品覆盖多个前端渠道(Web、App、小程序),其中身份认证和权限控制是最核心的一环。随着业务发展,原有的简易权限校验方式已经支撑不了日渐复杂的权限体系与安全性要求。

那一年我们启动了用户系统的重构计划,其中一个重点目标就是替换掉原来自己手写的登录拦截逻辑,引入成熟的安全框架进行统一管理。经过评估对比,最终选择了 Spring Security —— 它不仅功能强大,而且生态完善,在 Java 后端生态中几乎是标配级的存在。

但说实话,刚开始上手的时候我还是踩了不少坑,尤其是对它的过滤器链机制理解不到位,导致初期出现了一些严重的安全漏洞隐患。今天就想结合那次重构过程,把我们搭建安全认证系统的过程完整地复盘一下,希望对你有帮助。


遇到的问题:手写权限验证为何越来越难维持?

遇到的问题:手写权限验证为何越来越难维持?

在老系统里,我们用的是 Filter + 自定义 Token 校验的方式实现登录状态判断和权限控制。起初一切运行良好,但随着接口数量和业务场景增加,问题逐渐暴露出来:

  • 权限控制规则散落在各个地方,容易被遗漏或误配
  • 没有集中式的安全策略配置机制
  • Token 刷新、过期时间难以统一维护
  • 缺乏标准的异常处理流程,导致错误信息五花八门
  • 更严重的是,在某个新功能上线后不久,出现了因配置错误导致所有接口未授权访问的事件

这个事故直接推动了我们必须使用标准、可维护的安全框架来替代原有方案。


解决思路:选择 Spring Security 的原因

当时我们在对比 Spring Security 和 Apache Shiro,虽然 Shiro 上手更简单一些,但我们考虑了以下几个因素:

  1. 与 Spring Boot 天然兼容,无需额外集成适配;
  2. 支持现代认证方式(OAuth2、JWT、CAS 等);
  3. 强大的细粒度权限控制能力,可以按 URL、方法甚至参数进行配置;
  4. 社区活跃、文档丰富,遇到问题查资料也方便。

于是我们决定以 Spring Security 为核心,搭建一套全新的安全认证体系,配合 JWT 实现无状态鉴权。


架构设计与实现细节

整体架构概览

整个认证流程大致如下:

  1. 用户通过 /login 接口登录成功后,系统生成 JWT Token 返回;
  2. 前端将该 Token 放入请求头(Authorization: Bearer xxxx);
  3. 每次请求进入时,由 Spring Security 的过滤器链完成 Token 校验与权限匹配;
  4. 如果 Token 合法且具备访问权限,则放行;否则返回相应的错误码。

在这个流程背后,涉及到了数据库的设计、接口的规范、Token 的管理等多个方面。


数据库与接口设计

为了支持灵活的权限配置,我们需要两个关键表:

  • users:用户基础信息表(包含用户名、密码、账号状态等)
  • roles:角色表(例如 ADMIN、USER、GUEST)
  • user_role:用户-角色关联表
  • resources:资源表(每个 API 对应一个 resource)
  • role_resource:角色与资源的映射表

这套结构为后期通过动态加载资源权限提供了数据支撑。

至于接口设计,我们也遵循 RESTful 风格,并做了几条规范:

  • 所有接口必须携带有效 Token;
  • 登录接口不走权限过滤链;
  • 接口响应统一包装格式,错误码统一归类(如 401、403、500 等);
  • 日志记录完整的认证失败/成功事件,用于后续审计。

关键代码实践分享

下面我会挑几个最核心的部分贴出代码示例,便于你了解如何落地。

JWT 工具类(简化版)

@Component
public class JwtUtils {
    private String secret = "your-secret-key";
    
    public String generateToken(String username) {
        return Jwts.builder()
                .setSubject(username)
                .setExpiration(new Date(System.currentTimeMillis() + 864_000_000)) // 1天有效期
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
    }

    public String extractUsername(String token) {
        return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody().getSubject();
    }
}

自定义 JWT 过滤器

public class JwtFilter extends OncePerRequestFilter {

    @Autowired
    private UserService userService;

    @Autowired
    private JwtUtils jwtUtils;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain)
            throws ServletException, IOException {

        String token = extractToken(request);
        if (token != null && jwtUtils.validateToken(token)) {
            String username = jwtUtils.extractUsername(token);
            UserDetails userDetails = userService.loadUserByUsername(username);

            UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
                    userDetails, null, userDetails.getAuthorities());

            SecurityContextHolder.getContext().setAuthentication(authentication);
        }

        filterChain.doFilter(request, response);
    }

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

配置 Spring Security 主流程

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private JwtFilter jwtFilter;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
            .authorizeRequests()
            .antMatchers("/login").permitAll()
            .anyRequest().authenticated();
    }
}

这段配置是整个认证体系的核心逻辑,一定要理解清楚每一行的作用。


踩过的坑与解决经验

在实际开发过程中,我遇到了几个比较典型的问题,现在回想起来特别值得总结。

1. 忘记设置 Session 为无状态模式

一开始没有配置 .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS),结果 Spring Security 默认创建 session,造成缓存浪费不说,还有可能出现 session 丢失导致用户被迫重新登录的情况。后来加上之后问题迎刃而解。

2. JWT 校验失败不抛异常,而是直接忽略

由于我们的自定义 JWT Filter 中没有对 token 校验失败做统一的异常拦截,导致有些非法请求并没有触发 401,而是继续往下走。这个问题发现得晚,在测试环境几乎没暴露,直到灰度发布上线才发现问题。后来专门加了一个全局异常处理器统一拦截 JwtException 并返回正确的错误码。

3. 跨域请求导致预检失败

前后端分离后,跨域访问是个大问题。Spring Security 默认不允许跨域,如果不主动添加相关配置,会出现 OPTIONS 请求被拦截的现象。解决办法是在 configure() 方法中加入如下语句:

http.cors();

并配置对应的 CORS 允许域名:

spring:
  cors:
    allowed-origin-patterns: "https://yourdomain.com"

4. 用户权限更新后 Token 无法自动刷新

这是我们最初没想到的一个点。比如管理员修改了用户的角色,但在当前 Token 有效期内,用户的权限并不会变。为此,我们后来引入了 Token 黑名单机制,每次角色变更就将旧 Token 加入 Redis 进行拦截检查。


方案实施后的效果与收益

重构完成后,我们团队收获了很多积极反馈:

  • 权限逻辑更清晰:不再散落在各个 Filter 或 Controller 层;
  • 登录流程标准化:所有的认证都走统一入口;
  • 权限扩展性更强:新增角色和资源只需改数据库,无需改代码;
  • 生产运维成本降低:日志可追溯,错误类型统一化,排查更高效;
  • 安全性显著提升:再没发生过非授权访问的情况。

特别是在一次线上压力测试中,即便 QPS 达到 3000+,整个安全模块依然能稳定应对,几乎没有额外性能损耗。


给读者的经验建议

如果你正打算引入 Spring Security 或已经在用它,这里有几点我的实践经验想分享:

  1. 优先掌握过滤器链的工作机制
    Spring Security 的执行顺序是以过滤器链为核心的,不搞清这一点,很多问题根本无法定位。

  2. 尽量使用内置的异常处理机制
    Spring 提供了 AccessDeniedHandlerAuthenticationEntryPoint 接口来处理权限拒绝和未登录情况,不要自己随便 throw 异常。

  3. 不要低估权限模型的复杂度
    即使一开始只有一两个角色,也要预留好未来可能变化的空间。建议尽早建好 RBAC 模型。

  4. 多做本地测试+模拟生产环境演练
    Spring Security 有很多默认行为和隐藏规则,本地调试不充分,很可能在线上才会暴露问题。

  5. 关注 Spring Boot 版本兼容性
    不同版本的 Spring Boot 对 Security 的默认配置有变化,升级时要留意官方 changelog。


写在最后

数据流转过程-1

作为一个在一线摸爬滚打的后端开发者,我觉得 Spring Security 是必须掌握的一项技术。尽管它学习曲线陡峭,但我相信只要沉下心去一步步实践,一定能掌握它的精髓。

这次项目的重构让我深刻体会到:一个系统真正的“稳”,不止于高并发和低延迟,更在于基础安全做得牢靠。毕竟,再多的功能亮点,也可能因为一个安全漏洞而全部白费。

希望这篇文章能够帮你少走一些弯路,如果你也在做类似的事情,欢迎留言交流。一起在代码的世界里变得更“稳”一点吧。

评论 0

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