Spring Security基础:我的第一个安全认证系统实战记录

单元测试补习生
2025-06-22 03:17
阅读 566

大家好,我是林然,一个后端开发团队的负责人。今天我想聊聊我之前在做项目时遇到的一个典型问题——如何快速搭建一个安全、可靠的用户认证系统。这其实是很多项目的起点,尤其是在涉及权限管理、多角色登录等场景下,我们往往需要一个既灵活又稳定的认证框架。

这篇文章不是单纯的技术教程,而是结合我在实际工作中遇到的问题和解决方案的一次分享。希望通过这次复盘,不仅能帮你快速上手Spring Security,也能让你少走一些弯路。


背景介绍:为什么我们要从头开始做认证?

背景介绍:为什么我们要从头开始做认证?

事情发生在去年年底,当时我带的团队接手了一个全新的企业级后台管理系统。这个系统的用户群体比较复杂,包括普通员工、部门管理员、超级管理员等多个角色,权限划分也非常细致。最开始,我们打算直接用公司老项目里现成的认证模块来“套”进去,结果发现那块代码已经年久失修,逻辑混乱不说,扩展性极差,稍微一改就出bug。

于是我们决定:重写整个认证模块,采用Spring Security来做底层支撑。

说实话,刚开始我对Spring Security也是一知半解,只是听说过它功能强大,但真正深入用起来才发现它远比想象中要复杂得多,尤其是涉及到自定义登录流程、多角色鉴权、JWT集成这些高级用法的时候。

接下来我就按照我当时的思路,一步一步带你走进这段实战经历。


问题描述:认证逻辑混乱、权限控制不透明

问题描述:认证逻辑混乱、权限控制不透明

原来的认证模块主要存在以下几个问题:

  1. 登录验证逻辑分散:用户名密码验证、验证码校验、IP限制等功能被拆散在多个类中,维护困难。
  2. 权限控制依赖硬编码:有些接口的权限判断直接写在Controller中,修改权限就得重新上线。
  3. 无法支持多角色动态切换:前端传参切换角色,后端没有统一的抽象处理机制。
  4. 无日志记录和失败次数统计:一旦出现非法登录尝试,完全没办法追踪。
  5. 无法方便接入OAuth2或JWT:这对后期扩展是个大问题,特别是移动端或者第三方系统对接。

这些问题导致整个认证体系非常脆弱,动不动就出错,也容易留下安全隐患。


解决方案:使用Spring Security重构认证模块

解决方案:使用Spring Security重构认证模块

我们最终决定以Spring Security为核心,构建一个可扩展、易维护、安全性强的认证系统。下面是我在这次实践中的一些关键点总结:

一、选型与架构设计

我们的目标是做一个标准的前后端分离系统,所以认证部分需要用Token(比如JWT)代替Session,并且要兼容RBAC(基于角色的访问控制)模型。

技术栈:

  • Spring Boot + Spring Security
  • MyBatis + MySQL
  • JWT(JSON Web Token)
  • Redis(用于存储Token黑名单)

整体架构设计如下图所示:

[Login Request] → [AuthenticationFilter] → [CustomUserDetailsService]
                          ↓
                     数据库查用户信息
                          ↓
                   登录成功 → 生成JWT
                          ↓
                  返回Token给前端

所有请求必须携带Token,并在拦截器中完成鉴权。


二、Spring Security基本配置

首先,我们需要启用Spring Security并进行基本的配置。这里有几个关键点需要注意。

@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
            .csrf().disable()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .addFilterBefore(new JwtAuthFilter(userDetailsService(), jwtUtils), UsernamePasswordAuthenticationFilter.class)
            .authorizeRequests()
                .antMatchers("/login").permitAll()
                .anyRequest().authenticated()
            .and()
            .build();
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authConf) throws Exception {
        return authConf.getAuthenticationManager();
    }
}

这里有几个要点解释一下:

  • csrf().disable():因为我们是前后端分离,所以关闭CSRF防护;
  • sessionCreationPolicy(SessionCreationPolicy.STATELESS):告诉Spring不要创建Session;
  • 使用自定义的JWT过滤器替代默认的UsernamePasswordAuthenticationFilter。

三、自定义登录流程:从0到1实现JWT认证

这部分是最核心的部分,也是最容易出错的地方。

第一步:实现AuthenticationProvider

我们自己实现了一个JwtAuthenticationProvider,用来处理用户登录请求:

@Component
public class JwtAuthenticationProvider implements AuthenticationProvider {

    private final CustomUserDetailsService userDetailsService;
    private final PasswordEncoder passwordEncoder;

    public JwtAuthenticationProvider(CustomUserDetailsService userDetailsService, PasswordEncoder passwordEncoder) {
        this.userDetailsService = userDetailsService;
        this.passwordEncoder = passwordEncoder;
    }

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String username = authentication.getName();
        String rawPassword = authentication.getCredentials().toString();

        UserDetails userDetails = userDetailsService.loadUserByUsername(username);

        if (passwordEncoder.matches(rawPassword, userDetails.getPassword())) {
            return new UsernamePasswordAuthenticationToken(
                userDetails.getUsername(),
                null,
                userDetails.getAuthorities()
            );
        } else {
            throw new BadCredentialsException("用户名或密码错误");
        }
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return authentication.equals(UsernamePasswordAuthenticationToken.class);
    }
}

小贴士:记得把你的provider加进Spring Security的认证链中!

第二步:生成JWT Token

我们封装了一个JWT工具类,负责生成和解析Token:

public class JwtUtils {
    
    private static final String SECRET_KEY = "my-secret-key";
    
    public String generateToken(String username) {
        return Jwts.builder()
            .setSubject(username)
            .setExpiration(new Date(System.currentTimeMillis() + 864_000_000)) // 10天过期
            .signWith(SignatureAlgorithm.HS512, SECRET_KEY)
            .compact();
    }

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

第三步:实现JWT过滤器

我们在每次请求进来前,都会检查Header里的Token是否存在,如果存在则解析它并构造出当前用户的Authentication对象:

public class JwtAuthFilter extends OncePerRequestFilter {

    private final UserDetailsService userDetailsService;
    private final JwtUtils jwtUtils;

    public JwtAuthFilter(UserDetailsService userDetailsService, JwtUtils jwtUtils) {
        this.userDetailsService = userDetailsService;
        this.jwtUtils = jwtUtils;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String token = getTokenFromRequest(request);
        
        if (token != null && jwtUtils.validateToken(token)) {
            String username = jwtUtils.extractUsername(token);
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);

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

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

        filterChain.doFilter(request, response);
    }

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

系统架构设计图-1

别忘了把它添加到Security的过滤链中!


四、权限控制:从“能访问”到“有权限”

我们一开始的做法是在Controller里用注解控制权限:

@PreAuthorize("hasAuthority('ROLE_ADMIN')")
@GetMapping("/admin")
public ResponseEntity<?> adminOnlyEndpoint() {
    // 只有管理员能访问
}

但是这种写法虽然简单,却不够灵活,也不便于后期维护。后来我们就改成了更通用的方式——在请求到来时,通过数据库实时加载用户的权限列表,结合RBAC模型进行验证。

为此,我们设计了一张简单的权限表:

id name code description
1 用户管理 USER_MANAGE 管理用户信息
2 订单查看 ORDER_VIEW 查看订单详情

然后每个用户的角色会关联多个权限,在登录时将这些权限塞到Spring的GrantedAuthority中,这样后续就可以用@PreAuthorize("hasAuthority('ORDER_VIEW')")这样的方式来进行细粒度鉴权。


实施效果:更清晰、更可控、更安全

实施效果:更清晰、更可控、更安全

自从我们引入了Spring Security这套认证机制之后,系统有了以下几个显著变化:

  1. 权限控制更加直观:只需要在数据库配置权限码,就能控制接口访问;
  2. 登录逻辑高度解耦:所有的认证过程都被封装在独立的filter和provider中;
  3. 可扩展性强:后续我们可以轻松接入OAuth2、LDAP、多因素认证等机制;
  4. 日志完整、可追踪:我们将登录事件、异常操作都记录到了日志中,方便排查问题;
  5. 支持Token注销和刷新:利用Redis缓存黑名单,实现Token吊销功能。

更重要的是,整套机制运行稳定,再也没有出现因为认证问题导致的服务不可用。


我的经验和建议

如果你现在正在考虑是否要用Spring Security,或者刚入门不知道从哪开始,下面几点经验希望能帮到你:

✅ 1. 不要一开始就追求“全功能”

很多人第一次接触Spring Security就想一步到位写出完美的认证系统,结果被各种概念绕晕。其实你可以先从最基本的表单登录开始练手,再逐步加上JWT、Token管理等高级功能。

✅ 2. 多打印日志,了解每一步发生了什么

Spring Security是一个“魔法”很多的框架,你不去调试的话根本不知道哪个步骤出了问题。建议你在AbstractAuthenticationProcessingFilterOncePerRequestFilter这些地方加日志输出,观察流程走向。

✅ 3. 合理使用Filter链顺序

Spring Security内部有很多内置过滤器,你添加自己的filter一定要注意顺序。例如我们的JWT过滤器必须放在UsernamePasswordAuthenticationFilter前面,否则会导致重复执行。

✅ 4. 善用@PreAuthorize和表达式语言SpEL

Spring Security内置了很多强大的权限表达式,比如:

@PreAuthorize("#userId == authentication.principal.userId or hasAuthority('ADMIN')")

这类写法可以非常灵活地控制接口权限。

✅ 5. 提前规划好权限结构和角色模型

权限设计不能临时拍脑袋,一定要提前画好权限模型图,明确哪些角色对应哪些权限。否则后面会非常难维护。

✅ 6. 注意生产环境下的Token管理和黑名单机制

Token的安全性非常重要。除了设置合适的过期时间外,你还得考虑黑名单(Blacklist)、自动刷新、注销机制等。建议搭配Redis来做缓存。


写在最后:安全这件事,值得认真对待

回头看看那次项目经历,虽然一开始对Spring Security不熟悉,走了不少弯路,但现在回想起来,它确实是我们后端系统中不可或缺的一环。

特别是在现在这种微服务、API泛滥的时代,一个好的安全框架不仅能保护你的数据,还能提升整个系统的稳定性、可维护性和扩展性。

希望这篇来自一线实战的文章,能帮助你少踩几个坑,更快地上手Spring Security。如果你有任何问题,或者想了解更多细节,欢迎留言交流!

技术路上,我们共勉。

评论 0

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