用 Spring Security 快速搭建安全认证系统:从需求到上线的实战经验分享

极客小岛
2025-06-16 03:27
阅读 269

大家好,我是阿亮。最近在一个项目中遇到了一个典型的后端权限和认证问题,我负责的是用 Spring Boot + Spring Security 搭建一套完整的认证授权体系。说实话,刚开始我觉得这挺简单,不就是加个 @EnableWebSecurity 然后再写几个过滤器嘛?可真做起来才发现,理想很丰满,现实很骨感。

今天我就来聊聊我们是怎么一步步搞定这件事的,包括遇到的问题、踩过的坑,以及最终的效果。希望能帮助大家在实际项目中少走弯路。


背景与挑战:一个典型的 SaaS 系统安全需求

背景与挑战:一个典型的 SaaS 系统安全需求

项目的背景是一个企业级的 SaaS 系统,涉及多个用户角色(比如管理员、普通用户、访客),不同角色访问的资源不同,并且需要支持 OAuth2 的第三方登录,以及基于 Token 的 API 访问鉴权。

我们的初步目标:

  • 支持用户名密码登录
  • 支持基于 JWT 的无状态认证
  • 实现角色控制(RBAC)
  • 整合第三方登录(如微信、钉钉)
  • 统一处理未授权访问、超时登出等场景
  • 安全性要符合基本的防御标准(防 CSRF、XSS、SQL 注入等)

遇到的主要问题:

  1. Spring Security 的配置复杂,尤其是整合 JWT 后,很多地方都需要自定义。
  2. 权限模型设计不合理,导致后期改动成本高。
  3. Token 的刷新机制没考虑周全,出现部分接口反复返回 401。
  4. OAuth2 登录流程一开始走了弯路,最后才搞清楚是该自己实现还是用框架默认逻辑。
  5. 测试环境表现正常,生产部署却频频报错,查了半天发现是配置文件加载顺序的问题。

解决方案:Spring Security + JWT + OAuth2 的组合拳

解决方案:Spring Security + JWT + OAuth2 的组合拳

经过一轮调研和团队讨论,我们决定采用以下技术栈组合:

  • Spring Boot 2.7
  • Spring Security 5.7
  • JWT(使用 Java-JWT 库)
  • 数据库:MySQL + MyBatis Plus
  • 前端交互:Vue + Axios

整体架构上,我们采用了经典的分层结构:Controller -> Service -> Mapper,并在 Controller 层之上增加了 Security 过滤链,负责认证和鉴权。


架构图简述(伪代码版)

架构图简述(伪代码版)

[Client] → [Nginx/LB] → [Spring Boot]
                            │
                      ┌─────▼──────┐
                      │  Spring   │
                      │  Security │
                      └─────┬──────┘
                            │
            ┌─────────────┴──────────────┐
            ▼                             ▼
     UsernamePasswordAuthFilter    JwtAuthenticationFilter
            │                             │
       ↓ 用户名密码登录              ↓ Token 验证
           ↓                                 ↓
    UserDetailsService               TokenStore / Redis
           ↓
      权限验证(Role-based)
           ↓
         AccessDecisionManager

整个流程清晰易扩展。下面详细说一下实现的关键点。


实战编码:关键配置和代码片段

实战编码:关键配置和代码片段

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>

如果使用 OAuth2,还需要加入 spring-security-oauth2-resource-server


2. 自定义 JWT 工具类

我们封装了一个 JwtUtils,用来生成和解析 token:

public class JwtUtils {
    
    private static final String SECRET_KEY = "your-secret-key";
    private static final long EXPIRATION = 86400000; // 24h

    public static String generateToken(String username) {
        return Jwts.builder()
                .setSubject(username)
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION))
                .signWith(SignatureAlgorithm.HS512, SECRET_KEY)
                .compact();
    }

    public static String parseToken(String token) {
        return Jwts.parser()
                .setSigningKey(SECRET_KEY)
                .parseClaimsJws(token)
                .getBody()
                .getSubject();
    }
}

这里只是简单示例,实际项目中建议加上签发时间、刷新令牌等字段。


3. 编写自定义 JWT 过滤器

我们需要编写一个继承 OncePerRequestFilter 的类,用于拦截请求并验证 token:

@Component
public class JwtAuthenticationFilter 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 = JwtUtils.parseToken(token);
            UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
                    username, null, new ArrayList<>());
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        filterChain.doFilter(request, response);
    }

    private String extractToken(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }

    private boolean validateToken(String token) {
        try {
            JwtUtils.parseToken(token);
            return true;
        } catch (JwtException e) {
            return false;
        }
    }
}

4. 配置 Spring Security

接下来是最重要的 SecurityConfig 类:

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Autowired
    private JwtAuthenticationFilter jwtAuthenticationFilter;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/login").permitAll()
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated())
            .formLogin().disable()
            .httpBasic().disable();

        return http.build();
    }
}

可以看到,我们在这里做了几件事:

  • 关闭 csrf,因为我们是前后端分离架构
  • 使用无状态 session,配合 JWT
  • 加入自定义的 token 校验过滤器
  • 设置 /login 免权限访问
  • 角色为 ADMIN 可以访问 /api/admin/**
  • 禁用了表单登录和 Basic Auth,统一走 Token 鉴权

5. 接口设计:如何优雅地处理登录

为了简化流程,我们提供了一个通用的登录接口 /login

@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequestDto requestDto) {
    Authentication authentication = authenticationManager.authenticate(
        new UsernamePasswordAuthenticationToken(
            requestDto.getUsername(), requestDto.getPassword()
        )
    );
    SecurityContextHolder.getContext().setAuthentication(authentication);
    String token = JwtUtils.generateToken(requestDto.getUsername());
    return ResponseEntity.ok().header("Authorization", "Bearer " + token).build();
}

这里的 authenticationManager 是通过注入获取的,默认已经自动配置好了基于用户名密码的认证流程。


踩过哪些坑?

坑一:权限表达式写错了,导致所有接口都可以访问

最开始我在配置 .hasAuthority() 的时候,错误地写成了 .hasRole("admin"),但数据库里保存的角色是 "ADMIN"。结果就是永远进不去 admin 页面,后来才知道 hasRole() 默认会自动添加 "ROLE_" 前缀,所以应该是 .hasRole("ADMIN") 才对。


坑二:Token 刷新机制没做好,出现频繁掉线

初期我们用本地内存存 Token,导致服务重启就失效;后来改用 Redis 存储,但在集群环境下没有设置一致性过期策略,结果个别节点提前失效。解决办法是在每次请求都更新 Redis key 的 TTL,保持同步。


坑三:跨域请求被拦截了

前端 Vue 请求后端时,总是提示不允许的头部或方法。后来才发现是 CORS 的问题,在 Spring Security 中如果不显式放开 OPTIONS 请求,会被默认拦截。解决方案是在 SecurityConfig 中加上:

.cors().configurationSource(corsConfigurationSource())

并配置允许的域名、头信息和方法。


上线后的效果与优化建议

整个系统上线之后,性能和安全性表现都不错,QPS 在高峰期能维持在 3K 左右。我们还结合 Nginx 和 Redis 做了集群部署和 Session 同步。

不过随着用户量上涨,我们逐步做了以下几个优化:

  1. 增加 Token 黑名单机制:防止旧 Token 被盗用
  2. 引入分布式缓存管理:Redis 分布式锁控制并发操作
  3. 日志监控接入 ELK:追踪异常登录行为
  4. 定期更换密钥:提升长期安全性
  5. 接口粒度更细的权限控制:从接口级别细化到方法或字段级别

给开发者的几点建议

作为一线开发人员,我想分享一些亲身体会给正在看这篇文章的你:

  1. 不要死记硬背 Spring Security 配置,重点理解 Filter Chain 的执行流程。
  2. 安全设计要有前瞻性,别等到上线再补漏洞,代价太高。
  3. 多动手实践,少看教程照搬,否则遇到特殊情况就会束手无策。
  4. 结合项目实际情况选型,不是所有项目都需要 OAuth2,也不是所有都要 RBAC,按需定制最好。
  5. 安全不能只靠框架,应用层也要有意识,比如日志脱敏、输入校验、SQL 参数化处理等等。

总结

Spring Security 功能强大,但也“有点脾气”。要想用得好,不仅要掌握它的原理,还要结合具体业务场景灵活调整。尤其是在实际项目中,往往不只是简单的认证授权,还需要考虑性能、扩展性和安全性。

希望通过这篇实操经验的分享,能帮你避开那些“看着简单,用着难”的坑,快速搭起自己的安全认证系统。如果你也在用 Spring Security 或者准备开始接触它,欢迎留言交流,我们一起成长。


作者简介
阿亮,一名热爱写代码、热衷于分享经验的后端开发者,目前专注于企业级系统的架构设计和安全体系建设。业余喜欢写博客,偶尔也写点开源项目。欢迎关注我的 GitHub 和知乎专栏。

评论 0

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