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

报警声中醒来
2025-06-16 02:52
阅读 569

背景介绍

背景介绍

在上一个项目中,我们公司需要为一个新的后台管理系统搭建一套完整的用户权限体系。作为项目的主程,我负责整体的后端架构设计和安全机制实现。由于这个项目是内部使用的核心平台,涉及大量敏感数据和关键操作,因此系统的安全性显得尤为重要。

当时团队讨论了很多方案,最终决定采用 Spring Security 来构建整个认证与授权体系。虽然我们中间踩了不少坑,但也积累了许多宝贵的经验。今天我想借这篇文章,分享一下我在实际工作中是如何快速搭建并优化这套认证系统的,希望能给正在入门或想深入掌握 Spring Security 的同学一些启发和帮助。


我们面临的问题

我们面临的问题

1. 需求多且复杂

这个后台系统需要支持:

  • 用户名/密码登录
  • 基于角色的访问控制(RBAC)
  • 多种第三方登录方式(计划后续接入企业微信、LDAP等)
  • 登录失败次数限制与账户锁定机制
  • Token 过期自动刷新机制

但当时我们的团队对 Spring Security 并不熟悉,尤其是如何结合 JWT 实现无状态认证、如何高效地做权限控制这些高级用法。

2. 性能问题初探

最开始我们按照教程写了一个简单的基于数据库的登录接口,结果测试时发现,每次请求都要进行多次数据库查询,响应时间竟然达到了 300ms+,严重影响了用户体验。

这显然不符合我们对系统性能的要求。我们需要在不影响安全性的前提下,提升认证流程的整体效率。

3. 缓存和并发处理缺失

随着开发推进,我们在测试环境模拟高并发场景时,发现频繁的登录尝试会引发数据库连接池耗尽的问题。尤其是一些恶意攻击者可能会故意发起大量无效登录,导致系统瘫痪。

这些问题促使我们不断优化架构设计,引入缓存、防暴力破解策略,并重构认证逻辑。


解决方案:一步步搭建 Spring Security 认证系统

解决方案:一步步搭建 Spring Security 认证系统

为了更清晰地说明整个流程,我会从几个关键模块来展开讲:

第一阶段:基础搭建 —— 快速实现用户名密码登录

我们最初的目标很简单:让用户可以通过用户名密码登录成功,并返回一个 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>

2. 构建用户实体类

我们设计了一个 UserDetails 接口的实现类,用于存储用户信息和权限列表:

public class AuthUser implements UserDetails {
    private String username;
    private String password;
    private boolean enabled;
    private List<GrantedAuthority> authorities;

    // ...构造方法省略...
    
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return username;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return this.enabled;
    }
}

3. 自定义用户加载服务

接下来是实现 UserDetailsService 接口,通过数据库查询获取用户信息:

@Service
public class UserService implements UserDetailsService {

    @Autowired
    UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username);
        if (user == null) throw new UsernameNotFoundException("用户不存在");

        List<GrantedAuthority> authorities = new ArrayList<>();
        // 从数据库读取用户的角色权限,转换为 GrantedAuthority 对象
        for (String role : user.getRoles()) {
            authorities.add(new SimpleGrantedAuthority(role));
        }

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

4. 登录控制器实现

我们封装了一个登录 Controller,用来接受用户名和密码,并生成 JWT Token:

@RestController
@RequestMapping("/auth")
public class AuthController {

    @Autowired
    AuthenticationManager authenticationManager;

    @PostMapping("/login")
    public ResponseEntity<?> login(@RequestBody LoginRequest request) {
        Authentication authentication = authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword())
        );

        SecurityContextHolder.getContext().setAuthentication(authentication);

        String token = JwtUtils.generateToken(authentication.getName());
        return ResponseEntity.ok(token);
    }
}

到这里为止,最基本的登录功能就实现了。虽然还不能直接上线,但为我们后续扩展打下了基础。


第二阶段:集成 JWT + 无状态认证

为了满足前后端分离的架构需求,我们决定放弃 Session 机制,改用 JWT Token 进行无状态认证。

1. 添加 JWT 工具类

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

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

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

2. 加入过滤器链

我们自定义了一个 JwtRequestFilter,用来拦截所有请求中的 Token,并完成身份校验:

@Component
public class JwtRequestFilter extends OncePerRequestFilter {

    @Autowired
    private UserService userService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String token = extractToken(request);

        if (token != null && !isTokenValid(token)) {
            String username = JwtUtils.parseToken(token);
            UserDetails userDetails = userService.loadUserByUsername(username);

            UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
                    userDetails, null, userDetails.getAuthorities());

            SecurityContextHolder.getContext().setAuthentication(authentication);
        }

        filterChain.doFilter(request, response);
    }

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

    private boolean isTokenValid(String token) {
        try {
            Jwts.parser().setSigningKey("your-secret-key").parseClaimsJws(token);
            return false;
        } catch (JwtException e) {
            return true;
        }
    }
}

并在配置类中将它加入 Spring Security 的过滤器链:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    JwtRequestFilter jwtRequestFilter;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
    }
}

这样我们就实现了完整的一套 JWT + Spring Security 认证流程,所有的接口都自动携带 Token 校验,无需再手动判断。


第三阶段:权限控制与性能优化

1. 基于角色的权限控制

在控制器中,可以直接使用注解完成细粒度的权限控制:

@GetMapping("/users")
@PreAuthorize("hasRole('ADMIN') or hasRole('SUPER_ADMIN')")
public List<User> getAllUsers() {
    return userService.findAll();
}

当然,你也可以在 Service 或 DAO 层使用类似的方式控制权限。

2. 引入 Redis 缓存用户权限

早期我们的权限判断流程如下:

Token -> 提取 username -> 数据库查用户 -> 查角色 -> 判断是否允许访问

这一过程每次都涉及数据库操作,严重拖慢速度。

为此,我们在 UserService 中加入了 Redis 缓存机制,避免重复查询:

@Override
public UserDetails loadUserByUsername(String username) {
    String cacheKey = "user:" + username;
    Object cached = redisTemplate.opsForValue().get(cacheKey);
    if (cached != null) {
        return (UserDetails) cached;
    }


![系统架构设计图-1](https://code-guide.oss.shanghai.autogptai.club/common/file/download?name=date2025061602/1efc5004-755f-4a99-b976-9af515850856.jpg)


    User user = userRepository.findByUsername(username);
    if (user == null) throw new UsernameNotFoundException("用户不存在");

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

    AuthUser authUser = new AuthUser(user.getUsername(), user.getPassword(), user.isEnabled(), authorities);
    
    // 设置缓存,有效期 5 分钟
    redisTemplate.opsForValue().set(cacheKey, authUser, 5, TimeUnit.MINUTES);
    return authUser;
}

这样一来,很多权限判断就可以走缓存,大幅降低数据库压力。

3. 防暴力破解机制

对于登录接口来说,如果不对非法请求做处理,很容易成为攻击入口。我们采用了以下几种方式:

  • 记录失败次数到 Redis
  • 达到上限则临时封禁 IP
  • 异步日志记录失败登录行为
  • 发送邮件提醒管理员

部分代码示意:

public void handleFailedLogin(String username, String ip) {
    String key = "failed_login:" + username;
    Long count = redisTemplate.opsForValue().increment(key, 1);
    if (count >= MAX_LOGIN_ATTEMPTS) {
        String blockKey = "blocked_ip:" + ip;
        redisTemplate.opsForValue().set(blockKey, "blocked", 30, TimeUnit.MINUTES);
        sendAlertEmail(ip, username);
    }
}

这样可以在保障安全的同时减少误伤。


效果总结

效果总结

经过一段时间的打磨,这套安全认证系统逐渐稳定下来。以下是几个关键指标的变化:

指标 改造前 改造后
登录接口平均响应时间 310ms 90ms
并发登录能力 <100 QPS >1000 QPS
用户权限加载次数 每次请求都查数据库 95% 请求命中缓存

同时,系统具备了以下能力:

  • 支持 RBAC 角色模型
  • 可扩展支持 OAuth2 和 SSO
  • 登录防护机制完善
  • 易于后续迁移至微服务架构

更重要的是,这套认证机制已经成为我们多个项目的统一标准组件,大大提升了研发效率。


经验分享 & 建议

1. 不要一开始就追求完美

刚开始的时候我们也想一步到位,设计得非常复杂。结果发现很多东西其实在项目初期根本用不到,反而增加了理解和维护成本。

建议大家先从“能跑”的版本做起,比如只实现用户名密码登录 + JWT Token,其他复杂的权限控制可以后期逐步增加。


2. 缓存是性能优化的关键

像权限这种不经常变化的数据,一定要加缓存。否则每进来一个请求都查数据库,不仅性能差,还会埋下安全隐患。

另外,记得设置合理的过期时间,防止数据脏读或者长时间缓存带来的更新延迟。


3. 安全永远是第一位的

即使你的系统只有内部人员使用,也必须重视安全。Spring Security 的许多默认配置其实是比较宽松的,比如不强制 HTTPS、不启用 CSRF 等。

你可以参考 OWASP Top 10 常见漏洞,逐个排查自己的系统是否存在风险点。


4. 日志 + 监控必不可少

任何一次登录失败、权限拒绝都应该有日志记录。此外,我们还接入了 ELK 做日志分析,并设置了异常行为预警,比如短时间内大量失败登录触发告警。


5. 技术选型要考虑长期可维护性

我们曾经考虑过 Shiro,但最后还是选择了 Spring Security,因为它的社区活跃度更高,文档更齐全,而且和 Spring Boot 集成更自然。

如果你打算长期维护项目,技术栈的选择真的很重要,不要图一时方便。


写在最后

安全从来不是一个可以“临时添加”的功能,而应该是从架构设计之初就要重点考虑的部分。Spring Security 提供了一套非常强大的认证和授权工具,但在实际应用中,还需要结合项目特点灵活调整和优化。

在这次项目中,我深刻体会到一句话:“安全是个系统工程。”每一个细节的疏忽,都有可能酿成大错。希望我的这段经历,能够给你带来一些思考和启发。

如果你也在用 Spring Security,或者在考虑搭建自己的认证系统,欢迎留言交流,我们一起成长。


本文作者是一名有着多年 Java 后端开发经验的架构师,擅长系统安全、性能调优与分布式架构设计。工作之余喜欢写文章、分享开发经验和行业动态。

评论 0

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