用Spring Security快速搭建一个安全认证系统 —— 我的实战经验分享

单元测试补习生
2025-06-29 18:24
阅读 635

背景介绍:为什么要写这个?

背景介绍:为什么要写这个?

去年年底,我接手了一个企业内部管理系统的重构项目。原本的老系统是基于老旧的Shiro框架开发的权限系统,随着用户量的增长和需求的变化,暴露出了一些严重的问题,比如:

  • 权限配置复杂难以维护;
  • 多租户支持不完善;
  • 登录流程耦合度高、扩展性差。

为了提升整个系统的可维护性和安全性,我们决定迁移到 Spring Security + OAuth2 的方案上来,借助 Spring 生态体系的成熟能力快速构建一套安全可靠的认证体系。而这篇文章,就记录了我在项目初期搭建认证模块时的一些思考和实际操作过程。


遇到的问题:从“能跑就行”到“要稳要强”

遇到的问题:从“能跑就行”到“要稳要强”

在老系统中,我们的登录验证和权限控制都是手动实现的。虽然能运行,但每当我们想调整某个角色的权限时,往往需要重新部署整个服务,这在测试阶段还能接受,但在生产环境下简直是噩梦。

而且,在做功能升级的过程中,我们也发现,很多接口根本没有权限校验,甚至有些关键数据接口是匿名可访问的——这在合规要求越来越严的大背景下,风险巨大。

当时我们的目标很明确:

  • 快速构建一套基于Spring Boot的安全认证机制;
  • 实现基础的用户名密码登录;
  • 支持RBAC(基于角色的权限控制);
  • 后续能够轻松接入OAuth2实现单点登录或第三方授权。

技术选型与方案设计

技术选型与方案设计

首先,我们需要确定的是,这套认证系统要尽可能少地侵入业务逻辑,同时又要具备良好的扩展能力。

我们选择了 Spring Security 作为核心安全框架,原因如下:

  • 社区活跃,文档丰富;
  • 可以灵活定制各种过滤器链;
  • 内置支持基本的身份认证机制(如表单登录、Basic Auth等);
  • 与Spring Boot集成天然友好;
  • 后续可以方便地接入JWT、OAuth2、CAS 等方案。

架构图简述

浏览器/APP -> [FilterChain] -> SecurityContext -> AuthenticationManager -> UserDetailsService -> DB
                                               ↘ AccessDecisionManager -> 权限判断

整个架构的核心在于请求到达业务接口前的几个关键节点处理身份识别、权限校验和上下文管理。

数据库设计要点

为了配合 RBAC 模式,我们在数据库中设计了四张核心表:

  1. users:存储用户信息(包括加密后的密码)
  2. roles:定义所有角色
  3. user_roles:用户-角色关联
  4. permissions:权限资源定义(例如:user:read, role:write
  5. role_permissions:角色-权限关联

通过这样的设计,我们可以很容易地为不同角色配置不同的权限,并且在后续扩展中也方便进行权限细粒度控制。


搭建过程详解

服务器部署方案-1

接下来我将逐步说明如何用 Spring Security 快速搭建这样一个基础的安全认证系统。这里假设你已经有一个 Spring Boot 工程。

第一步:添加依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-config</artifactId>
</dependency>
<!-- 还有数据库相关依赖,比如JPA、MySQL驱动等 -->

Spring Boot Starter Security 默认会启用一整套保护策略,比如默认会拦截所有请求并要求登录。

第二步:自定义Security配置类

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Autowired
    private CustomUserDetailsService userDetailsService;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .authorizeRequests()
                .antMatchers("/login").permitAll()
                .anyRequest().authenticated()
            .and()
            .formLogin()
                .loginProcessingUrl("/login")
                .successHandler(authenticationSuccessHandler())
                .failureHandler(authenticationFailureHandler())
            .and()
            .logout()
                .logoutUrl("/logout")
                .addLogoutHandler(logoutHandler());

        return http.build();
    }

    // 自定义认证成功/失败处理器
    @Bean
    public AuthenticationSuccessHandler authenticationSuccessHandler() {
        return new CustomAuthenticationSuccessHandler();
    }

    @Bean
    public AuthenticationFailureHandler authenticationFailureHandler() {
        return new CustomAuthenticationFailureHandler();
    }

    @Bean
    public LogoutHandler logoutHandler() {
        return new CustomLogoutHandler();
    }

    @Bean
    public AuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setUserDetailsService(userDetailsService);
        provider.setPasswordEncoder(passwordEncoder());
        return provider;
    }

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

这段代码有几个关键点需要注意:

  • formLogin() 表示使用表单登录,默认路径是 /login
  • 使用 .successHandler().failureHandler() 是为了统一返回 JSON 格式的响应;
  • 设置了 SessionCreationPolicy.STATELESS 是因为我们希望未来接入 JWT 时更加方便;
  • 自定义了 AuthenticationProviderUserDetailsService 来支持数据库鉴权。

第三步:实现 UserDetailsService 接口

这是 Spring Security 认证的核心之一,用于根据用户名加载用户实体。

@Service
public class CustomUserDetailsService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException("用户不存在");
        }
        
        Set<GrantedAuthority> authorities = new HashSet<>();
        for (Role role : user.getRoles()) {
            authorities.add(new SimpleGrantedAuthority(role.getName()));
            for (Permission perm : role.getPermissions()) {
                authorities.add(new SimpleGrantedAuthority(perm.getCode()));
            }
        }

        return new org.springframework.security.core.userdetails.User(
            user.getUsername(),
            user.getPassword(),
            authorities
        );
    }
}

我们在这里一次性把该用户的全部权限都注入到了 UserDetails 中,这样后续的权限控制就可以直接基于这些 Authorities 来判断了。

第四步:权限控制(注解方式)

我们在 Controller 方法上加上注解来控制访问权限,例如:

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

当然也可以使用表达式组合,比如:

@PreAuthorize("hasAuthority('admin') or hasAuthority('manager')")

不过要记得在启动类上加上:

@EnableMethodSecurity(prePostEnabled = true)

否则这些注解不会生效。


遇到的一些坑和解决思路

坑一:CSRF 默认开启导致POST请求被拦截

刚开始的时候,我们一直以为是跨域问题,折腾了很久才发现是 CSRF 保护机制在作祟。解决方案很简单,在前后端分离场景下关闭即可。

坑二:权限不生效

有时候你会发现明明加了 @PreAuthorize("hasAuthority('xxx')"),但权限似乎不起作用。这时候你要检查两点:

  1. 是否开启了 @EnableMethodSecurity
  2. 返回的 UserDetails 是否真的带上了对应的 authority

还有一个常见错误是:SimpleGrantedAuthority 中的权限名拼写错了,或者没有完全匹配。

坑三:多数据源导致查询效率低

我们的用户和角色数据一开始放在主业务库中,结果在用户数量增加之后,登录变得非常慢。于是我们对这部分进行了缓存优化,引入了 Redis 缓存用户基本信息和权限树结构,减少了数据库压力。


实际效果与收益

经过两周时间的开发和测试,我们顺利上线了新的安全认证模块。上线后效果非常明显:

  • 登录流程更加清晰,权限边界更明确;
  • 权限修改后实时生效,不需要重启服务;
  • 日志系统能准确记录每一次登录尝试和权限拒绝事件;
  • 整个权限模型便于后续迁移至 JWT 和 OAuth2,节省了大量重复工作。

更重要的是,团队成员不再担心因权限问题带来的线上事故,大家可以把更多精力投入到业务功能开发中去。


经验总结与建议

如果你正在考虑搭建一个基于 Spring 的安全认证系统,我的建议如下:

✅ 推荐做法

  • 优先使用 Spring Security 提供的标准组件,减少重复造轮子;
  • 将权限逻辑从业务中剥离出来,避免硬编码;
  • 对于权限变更频繁的系统,建议引入动态权限刷新机制;
  • 做好日志追踪和审计功能,便于排查异常行为;
  • 预留扩展接口,方便后续接入OAuth2、SSO等功能。

⚠️ 避免踩雷

  • 不要在认证流程中掺杂复杂的业务逻辑;
  • 不要把敏感数据明文存储在 Token 或 Session 中;
  • 不要忽视 CSRF 和 XSS 的防护机制;
  • 不要忽视并发场景下的性能瓶颈,尤其在数据库查询和权限校验环节。

结语:技术的温度在于落地实践

回过头来看,这套安全认证系统只是整个项目中的一个小模块,但它的重要性不言而喻。安全不是附加项,而是系统设计之初就必须考虑清楚的基础建设。

希望通过这篇文章,能帮你少走些弯路,更快地上手 Spring Security。如果你正准备搭建自己的认证系统,不妨试着从上面这个最小可行方案开始,再一步步扩展。记住,稳定、简洁、可扩展,才是工程化的精髓所在。

如果文章对你有帮助,欢迎留言交流,我们一起在实践中打磨技术细节。

评论 0

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