从零搭建安全认证系统:Spring Security 实战经验分享

Node不想睡
2025-06-26 16:02
阅读 509

作为一名拥有五年工作经验的后端工程师,我参与过多个企业级系统的开发与维护。其中,构建用户权限体系、实现登录认证和接口访问控制一直是我工作中最核心、也是最容易出问题的部分之一。

今天我想聊的是——如何用 Spring Security 快速搭起一套安全、灵活、可扩展的安全认证系统。这不是什么“Hello World”级别的教程,而是基于我去年在某金融 SaaS 平台重构权限模块的真实经历来写的。

背景介绍:为什么我们需要重新做认证系统?

背景介绍:为什么我们需要重新做认证系统?

我们原来的认证逻辑比较简单粗暴:用户登录后返回一个 token,后面所有接口都靠这个 token 去鉴权。但随着业务发展,我们遇到了几个明显的问题:

  • token 过期管理混乱:前端缓存 token 导致频繁出现 401(未授权),用户体验差;
  • 缺乏权限分层机制:不同角色(比如客服、管理员、运营)之间没有清晰的访问边界;
  • 日志追踪困难:谁干了什么事,无从追溯;
  • 代码结构复杂:各种 if-else 判断权限、手写 JWT 解析逻辑,一改就出 bug。

这些问题逼着我们必须重新梳理整套安全架构,而 Spring Security 就是我们最终选择的核心工具。

选型对比:为什么是 Spring Security,而不是 Shiro 或 Sa-Token?

选型对比:为什么是 Spring Security,而不是 Shiro 或 Sa-Token?

在我过去项目中,我用过 Apache Shiro、也试过国产框架 Sa-Token,但这次我毫不犹豫地选择了 Spring Security,原因如下:

框架 易用性 可扩展性 社区活跃度 集成生态支持
Spring Security 极强 非常高 Spring Boot 天生一对
Apache Shiro 一般 不适合大型系统
Sa-Token 一般 国产轻量、文档友好

虽然 Spring Security 学习曲线陡峭一些,但它提供的细粒度控制能力太吸引人了。尤其是在处理 OAuth2、JWT、前后分离登录流程、多角色授权方面,它提供了非常成熟的解决方案,几乎涵盖了我们所有的使用场景。

实施过程:一步步搭建安全认证体系

数据库设计模型-1

Step 1:需求梳理与接口设计

我们在重构之前,先开了一场头脑风暴会,明确以下几点:

  • 用户类型有三种:平台管理员、客户管理员、普通员工
  • 系统资源需区分读写权限(查看报表 vs 编辑合同)
  • 支持单点登出和 Token 续签
  • 接口要区分公开访问(如注册页)、需登录访问、需特定权限访问

基于这些需求,我们将整个安全体系分为三个层次:

  1. 身份认证层(Authentication):验证用户是否合法;
  2. 授权层(Authorization):确定用户能干什么;
  3. 访问控制层(Access Control):根据 URL/方法进行拦截或放行。

Step 2:引入 Spring Security Starter

我们的项目是 Spring Boot 2.7.x 的版本,直接引入 starter 即可:

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

默认情况下,Spring Boot 会自动注入一个 Basic Auth 的安全机制,所有接口都会被保护。这对我们来说当然是不够的,所以需要自定义配置。

Step 3:自定义 WebSecurityConfigurerAdapter(注意:已废弃)

虽然现在官方推荐使用 SecurityFilterChain API(Spring Boot 2.7+ 开始逐渐迁移),但我们老项目还是用了经典的 WebSecurityConfigurerAdapter,如果你也在迁移到新版本,可以尝试下面这种写法:

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .addFilterBefore(new JwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
            .authorizeRequests()
            .antMatchers("/api/public/**").permitAll()
            .anyRequest().authenticated();

        return http.build();
    }
}

这种方式相比以前的 adapter 类更简洁,也更适合函数式编程风格。大家可以根据项目实际情况选用。

Step 4:JWT + Redis 实现 Token 机制

我们采用了 JWT + Redis 的组合方案:

  • 登录成功后生成 JWT Token,并将该 Token 存入 Redis 设置 TTL;
  • 后续每次请求携带 Token,解析用户信息;
  • 如果 Token 过期或被注销,则删除 Redis 中记录;
  • 每次访问前通过拦截器校验 Token 是否有效。

JWT 工具类大致如下(只展示关键部分):

public String generateToken(UserDetails userDetails) {
    Map<String, Object> claims = new HashMap<>();
    return Jwts.builder()
        .setClaims(claims)
        .setSubject(userDetails.getUsername())
        .setExpiration(new Date(System.currentTimeMillis() + expiration))
        .signWith(SignatureAlgorithm.HS512, secret)
        .compact();
}

Redis 的作用主要是为了实现黑名单机制和 Token 注销功能。如果只用 JWT 是无法做到真正意义上的“注销”的。

Step 5:权限管理细化到角色+接口级别

我们参考了 RBAC 模型(基于角色的访问控制),在数据库中设计了以下几个表:

-- 角色
roles (id, name)

-- 权限
permissions (id, name, code)

-- 角色和权限关联
role_permissions (role_id, permission_id)

-- 用户和角色关联
user_roles (user_id, role_id)

然后在每个 Controller 上加上注解:

@PreAuthorize("hasPermission('contract:write')")
@PostMapping("/contract")
public ResponseEntity<?> createContract(@RequestBody ContractDTO dto) {
    // ...
}

这里我们用到了 SpEL 表达式配合自定义权限判断器来控制访问,具体实现可以继承 PermissionEvaluator 接口。

Step 6:自定义异常处理 + 日志埋点

为了让安全拦截更“友好”,我们增加了统一的异常处理:

@RestControllerAdvice
public class SecurityExceptionAdvice {

    @ExceptionHandler(AccessDeniedException.class)
    public ResponseEntity<?> handleAccessDenied() {
        return ResponseEntity.status(HttpStatus.FORBIDDEN).body("无权操作");
    }

    @ExceptionHandler(AuthenticationException.class)
    public ResponseEntity<?> handleAuthError() {
        return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("身份验证失败");
    }
}

同时,在 Filter 中埋点记录用户行为:

String username = getUsernameFromToken(request);
log.info("User [{}] accessed URI: {}", username, request.getRequestURI());

这些日志帮助我们快速定位权限问题,对审计也非常有用。

Step 7:生产环境上线后的调优建议

我们部署后遇到不少坑,总结一下实战经验:

1. 数据库连接池设置合理

因为权限校验涉及数据库查询,我们一开始没做好连接池配置,导致并发时大量请求阻塞。后来改成了 HikariCP,并做了缓存优化:

  • 使用本地缓存缓存角色权限映射;
  • 减少每次请求都要查 DB 的频率;
  • 加上 Redis 缓存失效策略。

2. Token 有效期不宜过长

早期我们把 token 设为 7 天过期,结果出现了 token 泄露风险高、强制下线不好实现等问题。最终我们调整为 2 小时 + refresh token,refresh token 在 Redis 控制生命周期。

3. 日志不能忽略

安全相关的所有动作必须记录日志,包括登录失败、访问拒绝、token 过期等。日志字段建议包含:

  • 用户名
  • 请求路径
  • 时间戳
  • IP 地址
  • traceId

这样在排查问题时才能迅速定位。

成果与收益

缓存策略对比-2

经过一个月的开发与测试,我们完成了完整的安全认证系统重构:

  • 所有接口都有清晰的权限管控;
  • 登录流程稳定,支持 JWT + Refresh Token;
  • 异常处理统一规范,提升接口友好性;
  • 安全漏洞减少,审计日志完整;
  • 新增角色只需后台配置,不需要改代码。

后续我们也在此基础上接入了 OAuth2 第三方登录,甚至开始探索基于 Open Policy Agent(OPA)实现更动态的授权策略。

我的经验和小建议

  1. 不要怕踩坑,但要避免重复踩坑
    Spring Security 确实难,但它是“一旦掌握,终身受用”。别急着抄 GitHub 上的 demo,一定要理解它的设计思想(Filter Chain、SecurityContext、Principal 等)。

  2. 不要一开始就追求完美
    安全系统是一个长期演进的过程。先完成基本认证、登录、权限划分,再去考虑复杂的扩展点(如多租户、外部认证等)。

  3. 权限模型尽量抽象清晰
    权限码的设计最好统一格式,例如:{模块}:{操作},如 user:read, order:delete,方便后期对接第三方权限系统。

  4. 性能问题早暴露早解决
    认证、鉴权过程涉及多次数据库查询,务必做好监控,特别是线上高峰时段的表现。

  5. 技术是手段,不是目的
    有时候我们会沉迷于技术细节,而忘了用户的实际需求。比如是否真的需要那么复杂的权限控制?或者是否可以通过业务流程限制代替技术权限?

写在最后

写这篇文章除了想分享我的实战经验,其实还想鼓励一下刚入行的同学:Spring Security 虽然难,但它是值得你花时间去攻克的一座“高山”。

我也曾经面对满屏的 filterOrder() 和一堆 .and() 的配置感到无从下手,但现在回头看,这一切都是值得的。

在这个越来越重视数据安全和隐私合规的时代,一个强大而可控的安全认证系统,不仅是项目的护城河,更是你个人技术成长路上的重要里程碑。

希望这篇文章对你有所帮助。如果你有任何想法或疑问,欢迎留言交流!

评论 0

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