用Spring Security快速搭建安全认证系统 —— 一位全栈工程师的实战分享

胡建国
2025-06-23 00:39
阅读 596

引子:为什么选了Spring Security?

引子:为什么选了Spring Security?

还记得去年我在公司接了一个新项目,任务是从零开始搭建一套面向企业的内部管理系统。用户群体是公司员工,所以权限控制和安全性成了重中之重。一开始我们考虑过Shiro、或者自研权限框架,但权衡之后,最终决定使用 Spring Security 来构建整套认证授权机制。

说来惭愧,在此之前我对 Spring Security 的认识仅限于“听说过”,真正用起来才体会到它功能的强大与灵活。这篇文章就以我实际工作中的经验为主线,分享一下我们是怎么在两周内用 Spring Security 快速搭建起一个基础认证系统,并逐步演进到支持 RBAC 模型的过程。


背景描述:从简单到复杂的安全需求演进

背景描述:从简单到复杂的安全需求演进

刚开始系统的需求很明确:

  • 用户登录后能访问自己的资源;
  • 不同角色(管理员、普通用户等)看到的内容不同;
  • 系统需要记录用户的操作日志;
  • 后续可能会接入OAuth2或第三方认证;

看起来这些要求并不难,但如果要兼顾可扩展性和维护性,就需要提前做好设计准备。

我们在技术选型上选择了 Spring Boot + MyBatis Plus + MySQL 的组合。认证和权限部分则直接上了 Spring Security。虽然一开始有些学习成本,但越往后越觉得这个选择非常明智。


遇到的问题:入门门槛高、配置繁琐

遇到的问题:入门门槛高、配置繁琐

刚开始集成 Spring Security 的时候,遇到几个典型问题:

1. 默认拦截太“霸道”了怎么办?

默认情况下,Spring Security 会对所有请求都进行认证检查。但我们有些静态资源(比如 /css, /js)其实是想放行的。如果不处理好,浏览器会不停地弹出登录框,用户体验极差。

解决办法其实也不复杂,在 SecurityConfig 配置类中加入下面这段代码即可:

@Override
public void configure(WebSecurity web) throws Exception {
    web.ignoring().antMatchers("/css/**", "/js/**", "/images/**");
}

这样就可以让 Spring Security 不再对这些路径做任何处理。

2. 登录页被重定向搞懵了

我们自己写了个登录页面,但一访问就被跳转到默认的 /login 页面去了。这是因为在默认配置下,未认证访问受保护资源时会被自动引导到内置登录页。

为了解决这个问题,我们需要覆盖默认行为:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .authorizeRequests()
            .antMatchers("/", "/home").permitAll()
            .anyRequest().authenticated()
            .and()
        .formLogin()
            .loginPage("/login") // 指定自己的登录页
            .defaultSuccessUrl("/dashboard")
            .failureUrl("/login?error")
            .permitAll()
            .and()
        .logout()
            .logoutSuccessUrl("/login?logout")
            .permitAll();
}

这样一来,用户访问 /login 就会进入我们自定义的登录页面,而不是 Spring Security 自带的那个。

3. 用户数据存哪?数据库怎么设计?

一开始为了图快,我们用了内存中的用户配置:

@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
    auth.inMemoryAuthentication()
        .withUser("admin").password("{noop}123456").roles("ADMIN");
}

不过很快我们就发现,这种方式根本不适合生产环境。于是转向了基于数据库的用户存储方案。

我们的数据库结构如下:

CREATE TABLE `users` (
  `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
  `username` VARCHAR(50) NOT NULL UNIQUE,
  `password` VARCHAR(100) NOT NULL,
  `enabled` BOOLEAN DEFAULT TRUE
);

CREATE TABLE `authorities` (
  `username` VARCHAR(50) NOT NULL,
  `authority` VARCHAR(50) NOT NULL,
  FOREIGN KEY (username) REFERENCES users(username)
);

然后通过实现 UserDetailsService 接口,从数据库加载用户信息:

@Service
public class CustomUserDetailsService implements UserDetailsService {

    @Autowired
    private UserService userService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userService.findByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException("用户不存在");
        }
        return new org.springframework.security.core.userdetails.User(
                user.getUsername(),
                user.getPassword(),
                getAuthorities(user)
        );
    }

    private Collection<? extends GrantedAuthority> getAuthorities(User user) {
        List<GrantedAuthority> authorities = new ArrayList<>();
        for (Role role : user.getRoles()) {
            authorities.add(new SimpleGrantedAuthority(role.getName()));
        }
        return authorities;
    }
}

这样整个用户系统的生命周期就能很好地管理起来了。


实战演练:一步步搭建认证系统

实战演练:一步步搭建认证系统

第一步:初始化Spring Boot项目

使用 start.spring.io 创建 Spring Boot 工程,引入以下依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.2.2</version>
</dependency>

第二步:实现核心接口

创建 UserRole 实体类,并完成 UserService 和 Mapper 层的开发。

关键步骤是实现前面提到的 UserDetailsService 接口,并将其注册成 Bean。

第三步:配置SecurityConfig

这是一个典型的配置类,实现了基本的认证流程、权限控制和异常处理:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private CustomUserDetailsService userDetailsService;

    @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()
            .headers().frameOptions().disable()
            .and()
            .authorizeRequests()
                .antMatchers("/public/**").permitAll()
                .anyRequest().authenticated()
                .and()
            .formLogin()
                .loginPage("/login")
                .successHandler((request, response, authentication) -> {
                    // 自定义成功处理器,记录日志或其他操作
                    response.sendRedirect("/dashboard");
                })
                .failureHandler((request, response, exception) -> {
                    response.sendRedirect("/login?error");
                })
                .permitAll()
                .and()
            .logout()
                .logoutUrl("/logout")
                .addLogoutHandler((request, response, authentication) -> {
                    // 可以记录登出日志
                })
                .logoutSuccessHandler((request, response, authentication) -> {
                    response.sendRedirect("/login?logout");
                });
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/css/**", "/js/**", "/images/**");
    }
}

第四步:添加操作日志记录

我们利用 Spring AOP,在用户登录/登出前后做一些审计记录:

@Aspect
@Component
public class AuthLoggerAspect {

    private static final Logger logger = LoggerFactory.getLogger(AuthLoggerAspect.class);

    @AfterReturning("execution(* org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter.attemptAuthentication(..))")
    public void logSuccessfulLogin(JoinPoint joinPoint) {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        logger.info("用户 {} 成功登录", auth.getName());
    }

    @After("execution(* org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler.logout(..))")
    public void logUserLogout(JoinPoint joinPoint) {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        if (auth != null && auth.isAuthenticated()) {
            logger.info("用户 {} 成功登出", auth.getName());
        }
    }
}

这段代码虽然简单,但在日常运维中非常实用,尤其在排查问题时能省去很多沟通成本。


踩坑经验总结:那些年我们一起掉过的坑

坑1:密码加密方式不统一导致始终登录失败

最初我们没有统一密码加密方式,有的地方用的是 {noop},有的地方用了 BCryptPasswordEncoder。结果就是——明明用户名和密码是对的,却死活登录不进去。

后来我们彻底清除了旧密码,确保所有用户密码都走同一个加密策略,问题才得以解决。

坑2:跨域问题引发的403异常

我们系统前端部署在另一个端口上,当发起登录请求时,总是返回 403 Forbidden。查了很久才发现是 CSRF 保护机制作祟。

虽然我们最后选择关闭 CSRF(因为使用 JWT 方案后不再需要),但如果是传统 Session 模式,一定要合理配置 CORS 策略和 CSRF Token 校验。

坑3:Session超时处理不合理影响体验

系统上线初期,经常有用户反馈“点击按钮没反应”。最后发现问题在于 Session 超时后没有及时提醒用户重新登录,而是静默地中断了请求。

解决方案是在 Spring Security 中添加一个自定义的 ExpiredSessionStrategy,并在 Session 失效时返回 JSON 响应或重定向到登录页。


效果和收益:安全、可控、易维护

最终我们用不到两周时间,就完成了一个完整的基础认证系统。其带来的好处包括:

  • 所有用户必须登录才能访问业务模块;
  • 角色控制清晰,权限配置灵活;
  • 登录日志、登出记录完整,便于审计;
  • 后期方便拓展多因素认证、OAuth2、SSO等功能;
  • 架构可插拔,未来升级 JWT 或其他方案也较为平滑。

这套机制上线后运行稳定,几乎没有出现因权限引起的线上问题,极大地提升了整体系统的可用性和安全性。


经验建议:给正在起步的朋友几点忠告

如果你也在尝试用 Spring Security 搭建认证系统,这里有几个小建议供你参考:

  1. 别怕学得慢:Spring Security 功能强大,但也确实有点陡峭。建议从最简单的 inMemoryUserDetailsManager 开始实践,先理解认证流程。
  2. 重视数据库设计:权限模型不要急于求成,RBAC 是个不错的起点。
  3. 关注安全性细节:比如密码存储要用加密算法,避免明文;注意 Session 超时机制等。
  4. 善用日志和工具链:比如 Spring Boot Actuator 提供的 /actuator/security 端点能帮你实时观察安全状态。
  5. 提前预留扩展能力:像后面要加JWT、微信登录、钉钉扫码登录等,最好在架构设计之初就预留足够的灵活性。

写在最后

作为一名全栈开发者,我觉得 Spring Security 是后端安全方面绕不开的一道坎。刚开始可能觉得复杂,但一旦掌握,它的抽象设计、可插拔架构和社区生态会让你欲罢不能。

回头看看这短短两周的搭建过程,不只是完成了工作任务,更是一次对权限体系、安全机制的深入理解和认知。希望这篇文章能帮你在 Spring Security 的路上少踩几个坑,早点跑起来!

如有疑问,欢迎留言交流~ 🚀

评论 0

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