用 Spring Security 快速搭建安全认证系统,我在实际项目中踩过的坑与成长

何杰
2025-06-12 10:19
阅读 785

开篇:为什么我要写这篇关于 Spring Security 的文章?

开篇:为什么我要写这篇关于 Spring Security 的文章?

去年年底,我参与了一个企业级 SaaS 平台的后端重构项目。这个平台已经有几年历史了,原来的权限模型非常薄弱,用户管理、角色分配都分散在各个业务模块中,导致权限控制混乱、接口安全性低,甚至出现了几个生产级别的越权访问漏洞。

我们决定从架构层面彻底重构认证授权体系。在调研了几种方案之后,我们最终选择了 Spring Security + OAuth2 + JWT 这一组合来构建新的安全中心。这期间我遇到了不少问题,也学到了很多经验。

今天,我想以自己的实战经历为基础,和大家一起探讨如何用 Spring Security 搭建一个灵活、可扩展、性能良好的安全认证系统。内容不仅包括基础搭建流程,还包括我在真实开发过程中的思考、踩过的坑和解决思路。


项目背景:SaaS 平台的安全性挑战

项目背景:SaaS 平台的安全性挑战

系统概述

这是一个面向中小企业的财税管理系统,包含财务核算、发票管理、人事工资等多个模块。平台采用前后端分离结构(前端为 Vue.js,后端为 Spring Boot)。

原有痛点

  • 用户登录逻辑分散在多个微服务中,存在重复代码
  • RBAC 权限模型未统一,权限校验逻辑耦合严重
  • 接口缺乏统一的权限控制机制
  • 没有统一的 Token 管理方式,Token 注销和过期难以处理

面对这些问题,我们需要一套能够支撑多租户、支持第三方接入、具备良好扩展性的统一安全体系。


面临的挑战

在初期尝试使用 Spring Security 构建认证中心时,我们遇到了以下几个关键问题:

  1. 权限粒度过粗:传统的 hasRolehasAuthority 已无法满足复杂的业务需求。
  2. Token 维护困难:JWT 是无状态的,一旦签发就很难注销或更新,存在安全隐患。
  3. 跨服务认证问题:多个微服务如何共享同一个认证结果?
  4. 性能瓶颈预警:权限查询频繁访问数据库,影响响应速度。
  5. RBAC 模型设计不合理:原有的角色权限绑定过于僵化,无法应对多租户场景下的动态配置。

这些问题让我意识到:单纯的“照搬教程”是不够的,必须结合实际架构进行优化。


解决方案:Spring Security 构建统一认证中心

我们采用了以下技术栈和架构设计:

  • 核心框架:Spring Boot 2.7 + Spring Security 5.6
  • 认证方式:基于 OAuth2 的资源服务器 + 客户端凭证模式
  • Token 管理:JWT + Redis 缓存黑名单(用于注销)
  • 数据库:MySQL 存储用户信息、角色、权限关系
  • 缓存:Redis 用于存储活跃 Token 和权限缓存
  • 微服务通信:OpenFeign + 自定义拦截器完成上下文传递

整体架构如下:

          +------------------+        +------------------+
          |      Gateway     |        |   Auth Server    |
          |                --|--------|                  |
          +------------------+        +--------+---------+
                                                 |
                   +------------------------------v----------------------------+
                   |            Microservices (Resource Servers)             |
                   |         User APIs / Invoice APIs / HR APIs ...          |
                   |           Each service validates token via JWT          |
                   +----------------------------------------------------------+

所有服务通过网关对外暴露 API,网关将请求转发前验证 Token,并携带用户上下文信息到具体微服务中。真正的权限校验由各微服务自身完成。


关键实现:一步步搭建 Spring Security 安全认证系统

数据流转过程-1

1. 初始化项目结构

我选择的是 Spring Initializr 创建标准的 Spring Boot 项目,主要依赖如下:

<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>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

2. 实现核心过滤器链

Spring Security 的本质是一个过滤器链条,我们可以自定义其中的关键步骤。

自定义 JWT 登录过滤器

public class JwtLoginFilter extends UsernamePasswordAuthenticationFilter {
    
    private final AuthenticationManager authenticationManager;
    private final JwtUtil jwtUtil;

    public JwtLoginFilter(AuthenticationManager authenticationManager, JwtUtil jwtUtil) {
        this.authenticationManager = authenticationManager;
        this.jwtUtil = jwtUtil;
        setFilterProcessesUrl("/api/auth/login");
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) {
        // 从请求体读取用户名密码
        try {
            LoginRequest loginRequest = new ObjectMapper().readValue(request.getInputStream(), LoginRequest.class);
            return authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword())
            );
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response,
                                            FilterChain chain, Authentication authResult) {
        String token = jwtUtil.generateToken(authResult.getName());
        response.addHeader("Authorization", "Bearer " + token);
    }
}

自定义 JWT 请求拦截器

public class JwtRequestFilter extends OncePerRequestFilter {

    private final UserService userService;
    private final JwtUtil jwtUtil;

    public JwtRequestFilter(UserService userService, JwtUtil jwtUtil) {
        this.userService = userService;
        this.jwtUtil = jwtUtil;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
        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 = userService.loadUserByUsername(username);
            if (jwtUtil.validateToken(jwt, userDetails)) {
                UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
                    userDetails, null, userDetails.getAuthorities()
                );
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        }

        filterChain.doFilter(request, response);
    }
}

3. 配置 Spring Security

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private final UserService userService;
    private final JwtUtil jwtUtil;

    public SecurityConfig(UserService userService, JwtUtil jwtUtil) {
        this.userService = userService;
        this.jwtUtil = jwtUtil;
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .addFilter(new JwtLoginFilter(http.getSharedObject(AuthenticationManager.class), jwtUtil))
            .addFilterBefore(new JwtRequestFilter(userService, jwtUtil), UsernamePasswordAuthenticationFilter.class)
            .authorizeRequests()
            .antMatchers("/api/public/**").permitAll()
            .anyRequest().authenticated();
        return http.build();
    }

    @Bean
    public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
        return http.getSharedObject(AuthenticationManagerBuilder.class)
            .userDetailsService(userService)
            .build();
    }
}

踩坑经验分享

1. Token 注销难?—— 加入 Redis 黑名单机制

一开始我们直接使用 JWT,发现一旦签发后几乎无法撤销权限,尤其是当用户退出登录或账户被禁用时,旧 Token 仍可能继续有效一段时间。

解决方案:引入 Redis 黑名单机制。

每次签发新 Token 时,记录其 jti(唯一标识符),设置 TTL 为剩余有效期。在每次请求拦截时先检查该 Token 是否在黑名单中。

if (isTokenBlacklisted(token)) {
    throw new InvalidJwtException(JwtException.NO_TOKEN, "Token 已被注销");
}

2. 权限判断太慢?—— 引入缓存策略

由于每次接口调用都需要校验权限,频繁地访问数据库会造成延迟。我们对常用权限信息做了两级缓存(Spring Cache + Redis)。

@Cacheable(value = "permissions", key = "#userId")
public List<String> getUserPermissions(Long userId) {
    // 查询数据库获取权限列表
}

同时结合 AOP,在需要校验的接口上加上注解:

@HasPermission("can_edit_invoice")
@GetMapping("/invoices/{id}/edit")
public ResponseEntity<?> editInvoice(@PathVariable Long id) {
    ...
}

3. 多租户场景下角色冲突?—— 动态权限模型设计

我们的系统支持不同租户的管理员独立配置角色权限。这意味着同一名为“admin”的角色在不同租户下可能有不同的权限。

为此,我们在 JWT 中加入了 tenant_id 字段,并在权限加载时根据当前租户来过滤权限数据:

{
  "sub": "admin@example.com",
  "tenant_id": "12345",
  "roles": ["TENANT_ADMIN"],
  ...
}

4. 日志追踪丢失用户上下文?—— 结合 MDC 实现用户追踪

为了更好地排查问题,我们在每个请求拦截阶段都把用户信息放入 MDC(Mapped Diagnostic Context),方便日志追踪:

MDC.put("user", auth.getName());

这样,日志文件就能清晰地标明操作者是谁,极大提升了问题定位效率。


效果与收益总结

经过两个月的迭代,我们成功上线了全新的认证授权系统,带来显著提升:

  • 安全性增强:杜绝了越权访问,实现了统一权限控制
  • 开发效率提升:权限代码复用率提升 80% 以上,减少了重复劳动
  • 性能优化:平均请求耗时减少约 30%,缓存命中率达 95%
  • 可扩展性强:新增租户、角色、权限无需修改底层逻辑,只需修改配置即可生效
  • 运维成本下降:权限管理可视化程度提高,日志追踪更清晰

最重要的是,整个团队对 Spring Security 的掌控能力大幅提升,不再局限于“会用”,而是能深入理解其原理并做出合理调整。


我的几点建议

如果你也正准备使用 Spring Security 构建安全认证系统,这里是我走过弯路后的一些忠告:

1. 不要盲目复制模板代码

很多网上的示例都是“开箱即用”风格,但不一定适合你的业务场景。务必理解每一步的目的和作用,否则遇到问题时很容易懵圈。

2. 提前规划权限模型

权限模型的设计往往决定了整个系统的可维护性。推荐参考经典的 RBAC 模型,但也要根据业务特点适当扩展,比如加入多租户支持、细粒度权限等。

3. 把握好“集中 vs 分散”

虽然我们要追求统一,但在微服务架构下,权限判定其实最好下沉到业务层来做。认证可以统一,但具体权限应由各服务自行负责,避免耦合过度。

4. 性能考虑要前置

权限校验涉及数据库查询和 Token 验证,尤其在高并发下极易成为瓶颈。提前规划缓存、连接池、异步刷新策略至关重要。

5. 做好异常处理和日志审计

Spring Security 默认的异常返回格式很原始,建议自定义异常处理器,让错误信息更友好。此外,一定要记录关键事件,如登录失败、Token 注销、越权尝试等。


写在最后:技术人的安全感来自不断实践

说实话,在刚接手这个任务的时候,我心里也没底。毕竟 Spring Security 虽然名气大,但真的要用起来却并不轻松。它的灵活性和复杂性是一体两面:你既可以很快上手,也可以深陷其中。

但正是通过这次实战,我对权限体系的理解更加透彻,也开始学会从架构视角去权衡各种设计方案。这种成长比单纯的“学会某个工具”更有价值。

希望这篇文章能帮助你在搭建 Spring Security 安全系统的过程中少走些弯路。如果你也在做类似的事情,欢迎留言交流,一起进步。

毕竟,我们都在路上。

评论 0

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