Spring Security案例

wen java案例 2

Spring Security实战案例精解:从基础认证到OAuth2.0架构的完整落地

目录导读

  1. 案例背景与需求分析 - 为什么传统Filter无法满足现代安全需求
  2. 基础环境搭建 - Spring Boot + Spring Security 5.x 快速起步
  3. 核心案例一:基于内存用户的表单登录 - 手写配置类与密码加密
  4. 核心案例二:JWT无状态认证 - 前后端分离下的Token签发与验证
  5. 核心案例三:OAuth2.0社交登录 - 对接Github/微信的授权码模式
  6. 常见问题问答(FAQ) - 针对实战中的高频坑点解析
  7. 性能与安全优化建议 - 从CORS到CSRF的细节把控

案例背景与需求分析

现代Web应用面临的安全挑战早已超越简单的“登录+拦截”,一个典型的Spring Security案例需要解决三个核心痛点:认证(Authentication)授权(Authorization)防护(Protection),传统Servlet Filter虽然能做拦截,但无法提供方法级安全、OAuth2集成、以及Session固定攻击防护等高级能力。

Spring Security案例

Spring Security的核心价值在于其过滤器链(Filter Chain) 设计——它通过一组有序的过滤器(如UsernamePasswordAuthenticationFilterExceptionTranslationFilter)解耦了认证流程,本案例采用Spring Boot 2.7 + Spring Security 5.8组合,因为该版本在稳定性与函数式API之间取得了最佳平衡。

基础环境搭建

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

启动后,你会得到一个默认用户user及随机密码(打印在控制台),但实际案例中,我们需自定义配置:

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.csrf().disable()
            .authorizeHttpRequests(auth -> auth
                .antMatchers("/public/**").permitAll()
                .anyRequest().authenticated())
            .formLogin().permitAll()
            .and().logout().permitAll();
        return http.build();
    }
}

核心案例一:基于内存用户的表单登录

业务场景:小型内部管理系统,用户量少,采用配置文件硬编码。

@Bean
public UserDetailsService users() {
    UserDetails admin = User.withUsername("admin")
        .password(passwordEncoder().encode("secret123"))
        .roles("ADMIN")
        .build();
    return new InMemoryUserDetailsManager(admin);
}
@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

关键点:密码绝不能明文存储。BCryptPasswordEncoder每次加密结果不同(加盐),但matches()方法仍可校验。UserDetailsService负责从存储介质获取用户,但在内存版中需手动构造。

核心案例二:JWT无状态认证

业务场景:前后端分离,后端API无状态,需要支持跨域调用。

实现步骤

  1. 自定义JwtAuthenticationFilter,继承OncePerRequestFilter
  2. doFilterInternal中解析请求头Authorization: Bearer <token>
  3. 使用SecretKey签名验证token有效性
  4. 将封装的UsernamePasswordAuthenticationToken设置到SecurityContextHolder
public class JwtAuthFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res,
                                    FilterChain chain) throws IOException, ServletException {
        String header = req.getHeader("Authorization");
        if (header != null && header.startsWith("Bearer ")) {
            String token = header.substring(7);
            try {
                Claims claims = Jwts.parser()
                    .setSigningKey(SECRET_KEY)
                    .parseClaimsJws(token)
                    .getBody();
                String username = claims.getSubject();
                // 从数据库加载用户权限(此处简化为从claims取)
                var auth = new UsernamePasswordAuthenticationToken(
                    username, null, List.of(new SimpleGrantedAuthority("ROLE_USER")));
                SecurityContextHolder.getContext().setAuthentication(auth);
            } catch (JwtException e) {
                // token无效,不设置认证对象
            }
        }
        chain.doFilter(req, res);
    }
}

然后在SecurityConfig中把该过滤器替换默认的表单登录过滤器:

http.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);

核心案例三:OAuth2.0社交登录

业务场景:允许用户通过Github账号登录,免去注册流程。

spring:
  security:
    oauth2:
      client:
        registration:
          github:
            client-id: your-client-id
            client-secret: your-client-secret
            scope: read:user

配置中无需写代码,只需定义授权端点,登录后,默认的OAuth2UserService会将用户信息映射为OAuth2User,若需持久化用户,需实现OAuth2UserService重写loadUser方法。

注意:Github的client-idsecret需在Github开发者设置中申请,回调地址默认为/login/oauth2/code/github,此路径必须与Github后台配置一致。

常见问题问答(FAQ)

Q1:Spring Security 6相比5在配置上有何重大变化? A:6.0将WebSecurityConfigurerAdapter废弃,强制使用组件式注册(即SecurityFilterChain Bean)。antMatchers替换为requestMatchersand()链式调用改为lambda风格,若从5升级,需移除@EnableGlobalMethodSecurity旧写法,改用@EnableMethodSecurity

Q2:如何解决AJAX请求返回302而不是401? A:在SecurityConfig中配置http.exceptionHandling().authenticationEntryPoint((req, res, e) -> { res.sendError(HttpServletResponse.SC_UNAUTHORIZED); }),因为默认行为是重定向到登录页,对API不友好。

Q3:Session与JWT如何选择? A:原生Web应用建议使用Session(易于管理注销),前后端分离且需跨域共享认证时使用JWT,但JWT存在注销困难问题,需引入黑名单机制(如Redis存储失效token)。

Q4:CSRF防护为何在无状态API中被禁用? A:CSRF攻击依赖浏览器自动携带Cookie,JWT通常存于LocalStorage,手动加入请求头,故不受其威胁,若使用Session,必须保留csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())

性能与安全优化建议

  1. 使用@PreAuthorize进行方法级控制:在Controller方法上加@PreAuthorize("hasRole('ADMIN')"),比在URL配置更精细。
  2. 密码策略:强制使用Argon2PasswordEncoder(目前最安全的BCrypt增强版),并设置最小长度12位。
  3. CORS配置:当使用JWT且允许其他域名访问时,必须显式配置:
    http.cors().configurationSource(request -> new CorsConfiguration().applyPermitDefaultValues());
  4. Header安全:默认已包含X-Frame-Options: DENY等,但建议额外配置Content-Security-Policy防止XSS。
  5. 监控:集成spring-boot-starter-actuator,暴露/actuator/heapdump/actuator/threaddump时需用@PreAuthorize限定IP。

以上案例覆盖了从传统表单到现代无状态认证的完整演进路径,实际项目中,通常组合使用:用户登录用OAuth2,内部调用用JWT,敏感操作再叠加动态验证码,Spring Security的过滤器链就像乐高积木,理解每个过滤器的定位,即可灵活拼装出符合业务的安全架构。

抱歉,评论功能暂时关闭!