Spring Security基础:一次真实项目中的快速安全认证搭建

程序员的月亮
2025-06-13 18:48
阅读 557

开篇背景:为什么选择Spring Security?

开篇背景:为什么选择Spring Security?

去年年底,我参与了一个企业级SaaS平台的后端重构项目。这个平台面向中小型企业客户提供CRM服务,用户量已经突破10万,并且每天都在快速增长。随着业务发展,原有的权限系统显得愈发捉襟见肘,不仅代码混乱、难以维护,而且安全性存在严重漏洞——比如明文存储密码、越权访问等问题。

项目组决定从零开始重构整个认证和权限模块,同时需要在两周内完成基础功能上线,为后续RBAC模型做铺垫。时间紧迫,架构必须稳定、扩展性要好,我们最终选择了Spring Security作为核心安全框架。虽然之前有接触过Shiro和自己造过轮子,但在企业级项目中使用Spring Security还是头一回。

这篇文章想结合那次实际经历,聊聊我是如何用Spring Security快速搭建起一个基本安全认证系统的,中间踩过的坑,也包括一些个人理解和经验总结。


问题描述:从零到有,我们遇到了哪些挑战?

问题描述:从零到有,我们遇到了哪些挑战?

1. 时间紧任务重

我们需要在两周内把用户登录、注册、Token管理(JWT)、数据库加密等基础功能全部跑起来,不能拖慢主线开发节奏。

2. 安全性和规范性要求高

由于是SaaS产品,客户数据敏感,必须符合基本的安全规范:

  • 密码不得明文存储;
  • 接口需统一鉴权;
  • 登录失败限制机制;
  • 防止暴力破解攻击;
  • 支持OAuth2(预留);

3. 对接已有系统困难

老系统遗留的数据结构比较杂乱,字段命名风格不统一,迁移过来的接口还要兼容旧逻辑,对权限拦截机制提出了更高要求。

4. 技术选型与学习成本平衡

团队成员中有几位对Spring Security并不熟悉,既要保证实现正确,又要兼顾可维护性和交接文档的编写。


解决方案:技术选型和架构设计思路

解决方案:技术选型和架构设计思路

面对以上挑战,我们采用如下技术栈进行搭建:

技术点 技术选型
框架 Spring Boot 2.7 + Spring Security 5.7
数据库 MySQL + MyBatis Plus
Token管理 JWT
加密方式 BCryptPasswordEncoder
日志审计 Slf4j + AOP
安全增强 Rate Limiter + IP封禁策略

架构简图示意:

[客户端]
   |
[网关层/Nginx] --> [登录/注册接口]
   |                ↓
[Spring Security过滤器链] 
   |
[认证成功 → 返回Token / 认证失败 → 401]

整体架构偏向传统单体应用的前后端分离模式,但为了后期支持微服务架构,我们在Token生成和解析上做了预研,提前预留了刷新机制和跨域鉴权支持。


代码实践:关键配置和流程实现

代码实践:关键配置和流程实现

先来看主流程:登录 → 验证身份 → 返回Token → 后续请求携带Token自动鉴权

1. 添加依赖(pom.xml

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
  <groupId>io.jsonwebtoken</groupId>
  <artifactId>jjwt-api</artifactId>
  <version>0.11.5</version>
</dependency>
<dependency>
  <groupId>io.jsonwebtoken</groupId>
  <artifactId>jjwt-impl</artifactId>
  <version>0.11.5</version>
</dependency>
<dependency>
  <groupId>io.jsonwebtoken</groupId>
  <artifactId>jjwt-jackson</artifactId>
  <version>0.11.5</version>
</dependency>

2. 配置Security主类

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
    
    private final CustomUserDetailsService userDetailsService;
    private final JwtRequestFilter jwtRequestFilter;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
                .csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
                .addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class)
                .authorizeHttpRequests()
                .requestMatchers("/api/auth/**").permitAll()
                .anyRequest().authenticated()
            .and()
                .build();
    }

    @Bean
    public AuthenticationManager authenticationManagerBean(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

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

3. 自定义JWT验证过滤器

@Component
@RequiredArgsConstructor
public class JwtRequestFilter extends OncePerRequestFilter {

    private final JwtUtil jwtUtil;
    private final CustomUserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
        final String authorizationHeader = request.getHeader("Authorization");

        String username = null;
        String jwt = null;

        if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
            jwt = authorizationHeader.substring(7);
            username = jwtUtil.extractUsername(jwt);
        }

        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);

            if (jwtUtil.validateToken(jwt, userDetails)) {
                UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
                    userDetails, null, userDetails.getAuthorities()
                );
                token.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(token);
            }
        }

        chain.doFilter(request, response);
    }
}

4. 用户表结构设计(简化版)

CREATE TABLE `user` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `username` varchar(50) NOT NULL UNIQUE,
  `password` varchar(100) NOT NULL,
  `enabled` tinyint(1) DEFAULT '1',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB;

密码字段长度一定要够大(BCrypt加密后字符串较长),否则会抛出“Data too long”的异常!


踩坑经验:开发过程中的几个典型问题

❗️1. 忽略CSRF设置导致登录报错

刚开始的时候没有关闭CSRF,默认Spring Security会对所有POST请求做校验,结果第一次测试时登录接口就返回403 Forbidden。排查半天才想起要加.csrf().disable()

建议:如果是前后端分离的应用,推荐关闭CSRF,改用Token来做防CSRF攻击。

❗️2. Filter顺序错误,导致Token没生效

Spring Security的过滤链是有顺序的,如果不小心把JWT过滤器放在其它位置,会导致有些请求压根没走到鉴权逻辑。

正确做法:.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class) 这个位置才能覆盖默认的登录处理逻辑。

❗️3. JWT过期时间被系统时间干扰

一开始我们在Token里写死的是绝对时间戳,比如exp: System.currentTimeMillis() + 60_000,后来发现不同服务器之间时间可能存在误差,容易出现Token刚发出就被判定过期的情况。

解决方法:使用LocalDateTime.now().plusMinutes(30).toEpochSecond(ZoneOffset.UTC)这种更稳定的UTC时间处理方式。

❗️4. 密码加密字段长度不足

这是个老生常谈的问题:如果数据库字段不够长,插入的时候就会报错:“Data truncation: Data too long for column 'password' at row...”

BCrypt加密后的密文长度约为60位,MySQL字段至少要设成varchar(100),保险起见可以设到200。


效果总结:项目重构后的收益体现

从最初两周的快速搭建到后面持续迭代,这套基于Spring Security的安全架构为我们带来以下几个显著好处:

  • 稳定性强:Spring Security社区成熟,避免了重复造轮子,几乎没有出现重大BUG;
  • 易于扩展:如后续增加角色权限控制时,只需自定义GrantedAuthority即可;
  • 性能良好:通过Filter链控制访问,响应速度始终维持在合理范围内;
  • 兼容性好:和Swagger、MyBatis Plus等主流组件配合顺畅,无明显冲突;
  • 运维友好:完善的日志输出和异常处理机制,极大降低了线上排查成本。

最重要的一点是,重构后我们真正实现了“权限即服务”,为后续多租户体系打下了坚实基础。


经验分享:给读者几点真诚建议

如果你正在或打算尝试使用Spring Security来构建自己的认证系统,以下几点是我亲身体验之后想特别强调的:

✅ 不要怕复杂,要学会拆解流程

初学Spring Security很容易陷入“为什么那么多类?为什么流程这么绕?”的困惑。其实只要记住几个核心流程:

  • 用户提交账号密码 → 被封装成Authentication → 交给AuthenticationManager → 由Provider去验证 → 成功则放入SecurityContext

理解清楚这几个环节,剩下的就是按需定制细节,比如加Filter、替换PasswordEncoder等。

✅ 注册入口务必单独处理,不要交给Spring Security

很多教程会让你直接写登录接口调用AuthenticationManager.authenticate(...),但对于实际项目来说,登录之外往往还有注册、邮箱验证、短信验证码等步骤。这些最好单独设计接口,只在认证阶段才接入Security的流程。

✅ 日志和监控一定要跟上

特别是生产环境,建议记录每一次登录行为(成功与否都要记录),方便审计和排查。可以通过AOP监听AbstractAuthenticationEvent相关事件实现。

✅ 提前预留权限升级空间

目前我们的认证系统已跑通,下一步就是在现有基础上加上RBAC模型。Spring Security本身支持hasRole()hasAuthority()等方法,只要设计好权限表关联关系,后续扩展会很轻松。


写在最后:技术路上的成长点滴

说实话,当初接到这个任务时心里也有点发虚。毕竟Spring Security不是个小东西,尤其是它的“约定大于配置”特性让很多新手望而却步。但当你真正沉下心来,跟着官方文档走一遍Demo,再结合实际项目调试修改,你会发现它远没有想象中那么神秘。

这次重构经历让我深刻认识到,一个好的安全系统不仅是代码写得好不好,更重要的是设计是否合理、扩展是否灵活、是否能适应不断变化的需求。Spring Security给了我们一个非常坚实的起点,剩下的就是根据业务实际情况去打磨和完善。

希望这篇来自实战一线的分享,能够帮到那些正在尝试或者刚刚入门Spring Security的朋友。也欢迎大家留言交流,我们可以一起探讨更多关于安全架构的话题。

共勉!

评论 0

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