Spring Security基础:快速搭建安全认证系统

优雅_探险家
2025-06-23 13:54
阅读 663

从零到一:Spring Security实战搭建安全认证系统

从零到一:Spring Security实战搭建安全认证系统

去年我参与了一个企业级 SaaS 系统的重构项目,其中一个核心诉求就是“用户访问控制”必须做得既安全又灵活。当时我们团队的技术栈已经是 Spring Boot,所以顺理成章地选用了 Spring Security 作为安全框架的核心组件。

说实话,在这之前我对 Spring Security 的理解还停留在 “登录拦截” 和 “权限配置” 的表层,真正开始深入研究后才发现它的复杂程度远超想象,同时也非常强大——它不仅覆盖了从登录验证、权限管理、CSRF 防御等传统安全场景,还能通过扩展支持 OAuth2、JWT、动态权限策略等多种现代方案。

今天这篇文章我想用真实项目中的一个小模块来和你分享:我们是如何一步步使用 Spring Security 搭建一个基本但功能完整的安全认证系统的,并且过程中遇到的那些典型“坑”,我也毫无保留地分享出来。


背景:为什么需要重新设计安全系统?

这个 SaaS 系统原本有一个自定义的身份认证模块。由于早期为了快速上线赶进度,很多逻辑是硬编码在业务代码里的,比如:

if ("admin".equals(username) && "123456".equals(password)) {
    // 登录成功
}

随着用户规模增长和多角色体系的引入(普通用户、管理员、审计员等),这种简单粗暴的方式带来的问题逐渐暴露:

  • 密码存储不安全,明文保存在数据库中;
  • 身份判断逻辑分散,修改一处权限要动多个文件;
  • 扩展性差,没有统一的安全策略抽象层;
  • 缺乏日志与监控机制,异常行为难以追踪;
  • 无外部集成能力,未来想接入微信授权或钉钉认证根本做不到。

因此,我们决定重构整个安全认证系统,目标是实现:

  1. 用户账户密码安全存储;
  2. 支持 RBAC(基于角色的访问控制)模型;
  3. 提供对外接口鉴权能力;
  4. 可扩展性强,方便对接第三方平台认证;
  5. 异常登录行为可记录并触发告警;
  6. 接入性能要稳定,不影响整体响应时间。

我们最终选择的是 Spring Boot + Spring Security 的组合,结合 JWT 做 Token 认证,外加 Redis 存储会话信息。这套架构在我们的系统中已经跑了快一年,效果很好。


实施过程:Spring Security 初体验踩坑记

第一步:引入依赖,初始化基础结构

我们使用的 Spring Boot 版本是 2.7.x,对应的 Spring Security 是 5.7.x,Maven 配置如下:

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

添加完依赖之后,启动应用会发现所有接口都被拦截了,默认的登录页也激活了。这说明 Spring Security 已经生效。

但这远远不够。我们需要定制自己的登录流程、权限校验方式,还需要跳过部分开放接口(如 /login, /register)的拦截。

为此,我们创建了一个继承 WebSecurityConfigurerAdapter 的类进行配置(注意:如果你使用的是 Spring Boot 2.7+,此方式依然有效;如果使用更高版本,请改用基于组件注册的方式):

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/login", "/register").permitAll()
                .anyRequest().authenticated()
                .and()
            .formLogin()
                .loginProcessingUrl("/login")
                .successHandler(myAuthenticationSuccessHandler)
                .failureHandler(myAuthenticationFailureHandler)
                .and()
            .logout()
                .logoutUrl("/logout")
                .clearAuthentication(true)
                .invalidateHttpSession(true);
    }
}

这里有几个关键点要提一下:

  • formLogin() 不仅提供了默认登录页,还可以支持前端发起登录请求(POST /login);
  • 自定义的成功/失败处理器可以做日志记录或重定向;
  • 退出登录时一定要记得清除 Session 和 Authentication。

不过,这时候我们的用户信息还是从哪来的?这就涉及到另一个核心点:如何加载用户信息


第二步:用户信息加载机制 —— UserDetailsService

Spring Security 中负责用户信息获取的核心接口是 UserDetailsService,我们要实现它的 loadUserByUsername(String username) 方法:

@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("用户不存在");
        }

        List<SimpleGrantedAuthority> authorities = new ArrayList<>();
        for (Role role : user.getRoles()) {
            authorities.add(new SimpleGrantedAuthority(role.getName()));
        }

        return new User(user.getUsername(), user.getPassword(), authorities);
    }
}

注意几点:

  • 返回的对象是 UserDetails 类型,Spring Security 内部会用它来做比对;
  • 权限字段需要以 ROLE_ 开头才能被自动识别为“角色”;
  • 密码要经过加密处理后再传入构造函数,否则会出现“Bad credentials”。

说到加密,我们就不得不提下一个重点模块:


第三步:密码加密策略(PasswordEncoder)

Spring Security 提供多种密码加密方案,最推荐的是 BCryptPasswordEncoder,因为它每次加密结果都不一样,安全性高。

我们在配置类中定义 Bean:

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

并在注册时调用:

String rawPassword = "user123";
String encodedPass = passwordEncoder.encode(rawPassword);

这样一来,存储到数据库中的就是加密后的字符串,即便泄露也不会立刻被破解。

不过有个小插曲我得说下:最初我们把加密算法写到了 Service 层之外,导致测试环境用明文,线上加密,结果在登录的时候出现匹配失败,排查了很久才发现这个问题。

从此以后我们都约定,一切与密码相关的行为都应在服务层处理,不允许透出给 Controller 或其他模块


第四步:实现 Token 登录 —— 结合 JWT

前面讲的是传统的 Cookie + Session 登录模式。但在前后端分离架构中,我们更倾向于使用 Token 机制,例如 JWT。

为了让 Spring Security 支持 Token 登录,我们做了以下几步:

  1. 创建 JWT 工具类生成和解析 Token;
  2. 自定义 Filter,拦截 /login 请求并生成 Token;
  3. 在后续请求中加入 Token 校验逻辑;
  4. 修改 SecurityConfig 配置放行静态资源和公共接口。

这部分代码相对较多,但我挑选两个核心部分展示:

生成 Token(简化版)
public String generateToken(UserDetails userDetails) {
    Map<String, Object> claims = new HashMap<>();
    return Jwts.builder()
        .setClaims(claims)
        .setSubject(userDetails.getUsername())
        .setExpiration(new Date(System.currentTimeMillis() + JWT_EXPIRATION))
        .signWith(SignatureAlgorithm.HS512, JWT_SECRET)
        .compact();
}
自定义 Token 过滤器(继承 OncePerRequestFilter)

微服务架构示意图-2

public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

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

        String token = extractToken(request);
        if (token != null && validateToken(token)) {
            String username = extractUsername(token);
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);
            UsernamePasswordAuthenticationToken authentication = 
                new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }

        filterChain.doFilter(request, response);
    }
}

然后把这个 Filter 注册进 Spring Security 的链路中:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .addFilterBefore(new JwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
}

这样就实现了 Token 登录的基础逻辑。


项目落地中的一些实际经验

数据库设计考虑

用户的权限体系我们采用的是 RBAC(基于角色的访问控制)模型,涉及三张主要表:

  • user:用户基本信息;
  • role:角色表;
  • user_role:用户角色关联表;
  • permission:权限项(如 read, write);
  • role_permission:角色权限关联表;

这种设计的好处是易于扩展。如果我们后期想要增加权限级别的细粒度控制(比如按数据范围控制),只需修改 PermissionEvaluator 即可。

性能优化要点

  • 避免频繁查询数据库:我们使用 Redis 缓存 Token 对应的用户信息,提升登录态识别速度;
  • 减少同步锁竞争:在并发环境中,Token 解析要尽可能无状态,不要依赖共享变量;
  • 日志记录适度化:安全相关的事件要记录,但不能因为记录影响主流程性能;
  • 启用缓存过滤:针对 /login 接口加上滑动验证码或 IP 限流策略,防止暴力破解。

日志与安全监控

除了基本的日志记录,我们还做了几件事:

  1. 将登录失败次数写入 Redis 并设置短过期时间,达到上限时临时锁定账户;
  2. 所有登录成功/失败行为都打到日志文件,配合 ELK 进行分析;
  3. 设置定时任务扫描异常 IP 行为,触发告警;
  4. 审计敏感操作(如修改密码、删除账号)需记录上下文信息。

这些措施大大增强了系统的防御能力。


那些年我们踩过的坑

总结几个我在项目中亲自趟过的雷:

问题描述 原因 解决方法
登录时提示“Bad credentials”,但用户名密码正确 忽略了密码加密 检查是否在登录前对输入密码进行了加密
角色不生效,无法访问受限页面 Role 名称没加 ROLE_ 前缀 规范命名规则,或者在配置中指定角色前缀
CORS 问题导致前端无法访问接口 后端未开启跨域支持 使用 @CrossOrigin 注解或配置全局 CORS
多个 Filter 执行顺序混乱 Filter 添加位置不准确 使用 http.addFilterBefore() 明确顺序
退出后仍能访问受限接口 Session 未清除干净 设置 .clearAuthentication(true) 并注销 Session
Token 登录失效频繁 Expire 时间设置太短 动态刷新 Token 或延长有效期

还有一次,我错误地在 UserDetailsService 的返回对象中设置了空的角色列表,结果导致所有接口都无法访问,调试了整整两个小时才定位到问题 😅


最终成果与收益

现在我们的安全模块已经在生产环境中稳定运行了一年多,支撑着超过 10 万用户的日常使用。

具体收益如下:

  • 所有用户密码均已加密存储,符合基本安全合规要求;
  • 支持前后端分离架构下的 Token 登录和自动刷新;
  • 多角色权限体系清晰,便于维护和扩展;
  • 出现异常行为可快速定位溯源,降低安全风险;
  • 第三方系统可通过 OAuth2 协议无缝集成;
  • 整体性能稳定,平均每秒处理 200+ 登录请求没问题。

而且最重要的一点:当我们准备接入公司内部的统一认证中心时,只需新增一个适配模块即可完成整合,原有架构无需大改。


给开发者的建议与思考

如果你正在尝试使用 Spring Security 或者打算重构现有的安全机制,我的一些实践经验或许对你有用:

  1. 别急着搞复杂的功能。先从简单的账号密码登录做起,了解 Spring Security 的基本流程;
  2. 重视安全合规规范。即使只是个中小型项目,也要避免明文密码和简单加密;
  3. 权限设计要有扩展性。RBAC 是一个成熟模型,值得在大部分项目中使用;
  4. 尽早介入日志与审计设计。安全事件的回溯非常重要,否则出问题时你会后悔没早点准备;
  5. 关注社区生态和技术趋势。比如 Spring Security 新版本对 OAuth2 与 OpenID Connect 的支持已经越来越完善,适合考虑长期演进的系统接入;
  6. 适当封装公共模块。将 Token 工具类、Redis 操作类、权限注解等做成通用组件,提高复用率。

系统架构设计图-1

另外,如果你是刚接触 Spring Security 的同学,建议从官方文档和 GitHub 示例入手。虽然文档有点冗长,但它几乎涵盖了你能想到的所有使用场景。


写在最后:关于技术成长的思考

其实,安全认证这类技术模块,往往是很多人忽略的地方。大家更多时候关注的是业务代码、接口设计、性能优化,却很容易忽视系统的入口防护。

而一旦出现疏漏,可能就是一场灾难。

所以在我看来,一个好的架构师或者工程师,不仅要懂怎么写出漂亮的业务代码,还要具备“构建防线”的意识。

Spring Security 就像是一把钥匙,打开了通向现代安全体系的大门。掌握了它,不只是让你学会了个工具,更是让你站在巨人的肩膀上,去应对更加复杂的工程挑战。

希望这篇基于真实项目的分享能够帮到你,也欢迎留言交流你的经验和想法。

一起成长,一起变强 😊

评论 0

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