Spring Security 6.x 实战指南

Spring Security 6.x 实战指南

Spring Security 6.x 是 Spring Security 历史上最重要的版本升级,本指南深入解析其核心架构、Lambda DSL配置、过滤器链机制,以及与OAuth2、JWT的无缝集成方案。

一、版本重大变化概览

1.1 6.x vs 5.x 核心差异

替代
替代
简化
保持
Spring Security 版本对比
5.x 版本
6.x 版本
configure HttpSecurity
WebSecurityConfigurerAdapter
hasRole 需要手动加 ROLE_
PasswordEncoder
Lambda DSL 配置
Component-based 组件化配置
hasRole 自动添加 ROLE_
PasswordEncoder 不再强制

1.2 主要破坏性变更

变更项 5.x 6.x 影响
配置方式 extends WebSecurityConfigurerAdapter @Bean SecurityFilterChain 所有现有项目需迁移
Lambda DSL 可选 推荐 代码更简洁
hasRole() 需手动加 ROLE_ 前缀 自动添加 ROLE_ 需检查现有代码
authorizeHttpRequests 不存在 必须使用 权限配置必改
requestMatchers antMatchers 基于Ant风格但更灵活 路径匹配更精确

1.3 迁移路线图

5.x 配置
移除 WebSecurityConfigurerAdapter
改用 @Bean SecurityFilterChain
使用 Lambda DSL
authorizeHttpRequests
替换 antMatchers 为 requestMatchers
检查 hasRole / hasAuthority
6.x 兼容配置

二、核心组件体系

2.1 架构总览

SecurityFilterChain
+List
+boolean matches(HttpServletRequest)
<<interface>>
SecurityFilter
AuthenticationManager
+Authentication authenticate(Authentication)
ProviderManager
+List<AuthenticationProvider> providers
UserDetailsService
+UserDetails loadUserByUsername(String)
PasswordEncoder
+String encode(CharSequence)
+boolean matches(CharSequence, String)
UserDetails
+String getUsername()
+String getPassword()
+List<GrantedAuthority> getAuthorities()
+boolean isAccountNonExpired()
+boolean isAccountNonLocked()
+boolean isCredentialsNonExpired()
+boolean isEnabled()

2.2 SecurityFilterChain 详解

java 复制代码
/**
 * SecurityFilterChain 是 6.x 的核心配置单元
 * 替代了 5.x 的 WebSecurityConfigurerAdapter
 */
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    @Order(1)
    public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception {
        http
            // API 白名单
            .securityMatcher("/api/**", "/health/**")
            .csrf(AbstractHttpConfigurer::disable)
            .sessionManagement(session -> 
                session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/health/**").permitAll()
                .anyRequest().authenticated())
            .addFilterBefore(jwtAuthenticationFilter(), 
                UsernamePasswordAuthenticationFilter.class);
        
        return http.build();
    }

    @Bean
    @Order(2)
    public SecurityFilterChain webFilterChain(HttpSecurity http) throws Exception {
        http
            // Web 页面配置
            .securityMatcher("/**")
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/login", "/register").permitAll()
                .requestMatchers("/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated())
            .formLogin(form -> form
                .loginPage("/login")
                .defaultSuccessUrl("/dashboard"))
            .logout(logout -> logout
                .logoutSuccessUrl("/login?logout"));
        
        return http.build();
    }
}

2.3 默认过滤器链

Spring Security 6.x 默认包含以下过滤器(按执行顺序):

java 复制代码
// org.springframework.security.web.FilterOrderResolver
// 过滤器执行顺序(部分)

1. ChannelProcessingFilter        // 通道处理(HTTP/HTTPS)
2. WebAsyncManagerIntegrationFilter  // 异步管理集成
3. SecurityContextHolderFilter   // 安全上下文持有
4. WebSocketHandlerFilter        // WebSocket 处理
5. CorsFilter                    // CORS 处理
6. CsrfFilter                    // CSRF 防护
7. LogoutFilter                  // 登出处理
8. OAuth2AuthorizationRequestRedirectFilter  // OAuth2 重定向
9. UsernamePasswordAuthenticationFilter  // 用户名密码认证
10. ConcurrentSessionFilter      // 并发会话控制
11. BearerTokenAuthenticationFilter  // Bearer Token 认证
12. BasicAuthenticationFilter    // HTTP Basic 认证
13. RequestCacheAwareFilter      // 请求缓存
14. SecurityContextHolderAwareRequestFilter  // 安全上下文请求
15. AnonymousAuthenticationFilter // 匿名认证
16. ExceptionTranslationFilter   // 异常转换
17. AuthorizationFilter          // 权限授权(6.x 新增)

2.4 过滤器链执行流程

SecurityFilterChain ExceptionTranslationFilter AuthorizationFilter CsrfFilter CorsFilter HTTP请求 SecurityFilterChain ExceptionTranslationFilter AuthorizationFilter CsrfFilter CorsFilter HTTP请求 1. CORS 检查 2. CSRF Token 验证 Token 无效则拒绝 3. 遍历过滤器链 4. 权限授权判断 5. 权限不足则抛出 AccessDeniedException 6. 返回 403/登录页 7. 权限通过,继续处理

三、Lambda DSL 深度解析

3.1 Lambda DSL vs 传统配置

5.x 传统配置方式:

java 复制代码
// 5.x 配置(已废弃)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/public/**").permitAll()
                .antMatchers("/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            .and()
            .formLogin()
                .loginPage("/login")
                .defaultSuccessUrl("/home")
            .and()
            .csrf()
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
    }
}

6.x Lambda DSL 方式:

java 复制代码
// 6.x 配置
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/public/**").permitAll()
                .requestMatchers("/admin/**").hasRole("ADMIN")  // 自动添加 ROLE_ 前缀
                .anyRequest().authenticated())
            .formLogin(form -> form
                .loginPage("/login")
                .defaultSuccessUrl("/home"))
            .csrf(csrf -> csrf
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()));
        
        return http.build();
    }
}

3.2 常用配置示例

3.2.1 HTTP Basic 认证
java 复制代码
@Bean
public SecurityFilterChain basicAuthFilterChain(HttpSecurity http) throws Exception {
    http
        .securityMatcher("/api/**")
        .authorizeHttpRequests(auth -> auth
            .anyRequest().authenticated())
        .httpBasic(basic -> basic
            .realmName("My API"));
    
    return http.build();
}
3.2.2 表单登录
java 复制代码
@Bean
public SecurityFilterChain formLoginFilterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(auth -> auth
            .requestMatchers("/login", "/error", "/css/**", "/js/**").permitAll()
            .anyRequest().authenticated())
        .formLogin(form -> form
            .loginPage("/login")
            .loginProcessingUrl("/doLogin")
            .usernameParameter("username")
            .passwordParameter("password")
            .defaultSuccessUrl("/home", true)
            .failureUrl("/login?error")
            .successHandler(loginSuccessHandler())
            .failureHandler(loginFailureHandler()))
        .logout(logout -> logout
            .logoutUrl("/logout")
            .logoutSuccessUrl("/login?logout")
            .addLogoutHandler(logoutSuccessHandler())
            .invalidateHttpSession(true)
            .deleteCookies("JSESSIONID"));
    
    return http.build();
}
3.2.3 OAuth2 社交登录
java 复制代码
@Bean
public SecurityFilterChain oauth2FilterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(auth -> auth
            .requestMatchers("/", "/login/**", "/error").permitAll()
            .anyRequest().authenticated())
        .oauth2Login(oauth2 -> oauth2
            .loginPage("/login/oauth2")
            .authorizationEndpoint(authorization -> authorization
                .baseUri("/oauth2/authorization"))
            .redirectionEndpoint(redirection -> redirection
                .baseUri("/login/oauth2/code/*"))
            .userInfoEndpoint(userInfo -> userInfo
                .oidcUserService(oidcUserService())
                .userService(oAuth2UserService())
                .customUserType(GoogleUser.class, "google"))
            .successHandler(oAuth2AuthenticationSuccessHandler())
            .failureHandler(oAuth2AuthenticationFailureHandler()))
        .oidcLogin(OidcConfigurer -> {})
        .tokenEndpoint(token -> token
            .accessTokenResponseClient(accessTokenResponseClient()));
    
    return http.build();
}
3.2.4 JWT 无状态认证
java 复制代码
@Bean
public SecurityFilterChain jwtFilterChain(HttpSecurity http) throws Exception {
    http
        .csrf(csrf -> csrf.disable())
        .sessionManagement(session -> session
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS))
        .authorizeHttpRequests(auth -> auth
            .requestMatchers("/api/auth/**").permitAll()
            .anyRequest().authenticated())
        .oauth2ResourceServer(oauth2 -> oauth2
            .jwt(jwt -> jwt.decoder(jwtDecoder())));
    
    return http.build();
}

@Bean
public JwtDecoder jwtDecoder() {
    return JwtDecoders.fromIssuerLocation("http://auth-server");
}

3.3 常用配置方法对照表

5.x 方法 6.x Lambda DSL 说明
authorizeRequests() authorizeHttpRequests() 权限配置
antMatchers() requestMatchers() 路径匹配
permitAll() permitAll() 允许访问
hasRole() hasRole() 角色判断(自动加 ROLE_)
hasAuthority() hasAuthority() 权限判断(不加前缀)
anyRequest().authenticated() anyRequest().authenticated() 其他请求需认证
formLogin().loginPage() formLogin(form -> form.loginPage()) 登录页
httpBasic() httpBasic(basic -> {}) HTTP Basic
csrf().disable() csrf(csrf -> csrf.disable()) 禁用 CSRF

四、认证机制深度解析

4.1 AuthenticationManager 体系

<<interface>>
AuthenticationManager
+authenticate(Authentication)
ProviderManager
-List<AuthenticationProvider> providers
-AuthenticationManager parent
<<interface>>
AuthenticationProvider
+authenticate(Authentication)
+supports(Class<?>)
DaoAuthenticationProvider
-UserDetailsService userDetailsService
-PasswordEncoder passwordEncoder
JwtAuthenticationProvider
OAuth2LoginAuthenticationProvider

4.2 认证流程源码解析

java 复制代码
// org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
public class UsernamePasswordAuthenticationFilter extends
        AbstractAuthenticationProcessingFilter {
    
    public Authentication attemptAuthentication(
            HttpServletRequest request, 
            HttpServletResponse response) throws AuthenticationException {
        
        // 1. 判断是否是 POST 请求
        if (postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationException("Authentication method not supported") {};
        }
        
        // 2. 获取用户名和密码
        String username = obtainUsername(request);
        String password = obtainPassword(request);
        
        // 3. 创建认证 Token
        UsernamePasswordAuthenticationToken authRequest = 
            new UsernamePasswordAuthenticationToken(username, password);
        
        // 4. 设置详细信息
        setDetails(request, authRequest);
        
        // 5. 委托给 AuthenticationManager
        return this.getAuthenticationManager().authenticate(authRequest);
    }
}
java 复制代码
// org.springframework.security.authentication.ProviderManager
public class ProviderManager implements AuthenticationManager, 
        MessageSourceAware, InitializingBean {
    
    public Authentication authenticate(Authentication authentication) 
            throws AuthenticationException {
        
        Class<? extends Authentication> toTest = authentication.getClass();
        
        for (AuthenticationProvider provider : providers) {
            // 1. 检查是否支持该认证类型
            if (!provider.supports(toTest)) {
                continue;
            }
            
            try {
                // 2. 执行认证
                Authentication result = provider.authenticate(authentication);
                
                // 3. 认证成功,复制其他属性
                if (result != null) {
                    copyDetails(authentication, result);
                    eraseCredentials(result);
                    return result;
                }
            } catch (AccountStatusException e) {
                // 账户状态异常
                prepareException(e, authentication);
                throw e;
            } catch (AuthenticationException e) {
                // 继续尝试其他 Provider
                lastException = e;
            }
        }
        
        // 4. 所有 Provider 都认证失败
        if (lastException == null) {
            lastException = new ProviderNotFoundException(noProvidersMessage);
        }
        
        parentAuthenticationFailed(result, authentication);
        throw lastException;
    }
}

4.3 自定义认证逻辑

4.3.1 自定义 UserDetailsService
java 复制代码
@Service
public class CustomUserDetailsService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) 
            throws UsernameNotFoundException {
        
        // 1. 查询用户
        User user = userRepository.findByUsername(username)
            .orElseThrow(() -> new UsernameNotFoundException(
                "User not found: " + username));
        
        // 2. 构建权限列表
        List<GrantedAuthority> authorities = user.getRoles().stream()
            .map(role -> new SimpleGrantedAuthority("ROLE_" + role.getName()))
            .collect(Collectors.toList());
        
        // 3. 返回 UserDetails
        return User.builder()
            .username(user.getUsername())
            .password(user.getPassword())
            .authorities(authorities)
            .accountExpired(false)
            .accountLocked(user.getLocked())
            .credentialsExpired(false)
            .disabled(!user.getEnabled())
            .build();
    }
}
4.3.2 自定义 AuthenticationProvider
java 复制代码
@Component
public class SmsCodeAuthenticationProvider implements AuthenticationProvider {

    private UserDetailsService userDetailsService;

    @Override
    public Authentication authenticate(Authentication authentication) 
            throws AuthenticationException {
        
        SmsCodeAuthenticationToken token = (SmsCodeAuthenticationToken) authentication;
        
        // 1. 验证短信验证码
        String phone = token.getName();
        String smsCode = (String) token.getCredentials();
        
        if (!validateSmsCode(phone, smsCode)) {
            throw new BadCredentialsException("Invalid SMS code");
        }
        
        // 2. 加载用户信息
        UserDetails user = userDetailsService.loadUserByUsername(phone);
        
        // 3. 创建认证成功的 Token
        SmsCodeAuthenticationToken result = 
            new SmsCodeAuthenticationToken(user, smsCode, user.getAuthorities());
        result.setDetails(token.getDetails());
        
        return result;
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
    }
}
4.3.3 自定义认证过滤器
java 复制代码
public class SmsCodeAuthenticationFilter extends 
        AbstractAuthenticationProcessingFilter {
    
    public static final String PHONE_PARAMETER = "phone";
    public static final String SMS_CODE_PARAMETER = "smsCode";
    
    public SmsCodeAuthenticationFilter() {
        super(new AntPathRequestMatcher("/login/sms", "POST"));
    }

    @Override
    public Authentication attemptAuthentication(
            HttpServletRequest request, 
            HttpServletResponse response) throws AuthenticationException {
        
        String phone = request.getParameter(PHONE_PARAMETER);
        String smsCode = request.getParameter(SMS_CODE_PARAMETER);
        
        if (phone == null || smsCode == null) {
            throw new AuthenticationException("Phone or SMS code is null") {};
        }
        
        SmsCodeAuthenticationToken authRequest = 
            new SmsCodeAuthenticationToken(phone, smsCode);
        
        setDetails(request, authRequest);
        
        return this.getAuthenticationManager().authenticate(authRequest);
    }
}

五、授权机制深度解析

5.1 权限模型

<<interface>>
GrantedAuthority
+getAuthority() : String
SimpleGrantedAuthority
-String authority
<<interface>>
UserDetails
+getAuthorities()
<<interface>>
Authentication
+getAuthorities()
+getPrincipal()

5.2 hasRole vs hasAuthority

java 复制代码
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                // hasRole: 自动添加 ROLE_ 前缀
                // 用户需要 ROLE_ADMIN 权限才能访问
                .requestMatchers("/admin/**").hasRole("ADMIN")
                
                // hasAuthority: 不添加前缀,使用精确权限名
                .requestMatchers("/user/**").hasAuthority("USER_READ")
                
                // hasAnyRole: 任意一个角色即可
                .requestMatchers("/manager/**").hasAnyRole("ADMIN", "MANAGER")
                
                // hasAnyAuthority: 任意一个权限即可
                .requestMatchers("/report/**").hasAnyAuthority(
                    "REPORT_VIEW", "REPORT_EXPORT")
                
                // 权限表达式
                .requestMatchers("/api/data/**").access((context, object) -> {
                    Authentication auth = context.getAuthorizationManager()
                        .getAuthorizationManager();
                    return new AuthorizationDecision(
                        auth.getAuthorities().stream()
                            .anyMatch(a -> a.getAuthority().equals("DATA_ACCESS")));
                })
                
                .anyRequest().authenticated());
        
        return http.build();
    }
}

5.3 基于表达式的权限控制

java 复制代码
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                // 使用 SpEL 表达式
                .requestMatchers("/admin/**").access((authentication, object) -> {
                    Authentication auth = authentication.get();
                    boolean isAdmin = auth.getAuthorities().stream()
                        .anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN"));
                    
                    // 检查 IP 地址
                    HttpServletRequest request = authentication.getRequest();
                    String ip = request.getRemoteAddr();
                    boolean isInternalIp = ip.startsWith("192.168.");
                    
                    return new AuthorizationDecision(isAdmin && isInternalIp);
                })
                
                // 使用 hasRole 或其他标准规则
                .requestMatchers("/api/**").hasRole("USER")
                .anyRequest().authenticated());
        
        return http.build();
    }
}

5.4 方法级安全注解

5.4.1 启用方法级安全
java 复制代码
@Configuration
@EnableMethodSecurity(
    prePostEnabled = true,
    securedEnabled = true,
    jsr250Enabled = true
)
public class MethodSecurityConfig {
    // 配置类
}
5.4.2 使用安全注解
java 复制代码
@Service
public class UserService {

    // 需要 ADMIN 角色
    @RolesAllowed({"ADMIN", "SUPER_ADMIN"})
    public void deleteUser(Long userId) {
        userRepository.deleteById(userId);
    }

    // 需要特定权限
    @Secured("ROLE_USER_READ")
    public User getUser(Long userId) {
        return userRepository.findById(userId)
            .orElseThrow(() -> new UserNotFoundException(userId));
    }

    // 使用 SpEL 表达式
    @PreAuthorize("hasRole('USER') and #userId == authentication.principal.userId or hasRole('ADMIN')")
    public User updateUser(Long userId, UserUpdateRequest request) {
        // 检查当前用户是否有权修改
        return userRepository.findById(userId)
            .map(user -> {
                user.setEmail(request.getEmail());
                return userRepository.save(user);
            })
            .orElseThrow(() -> new UserNotFoundException(userId));
    }

    // 使用 SpEL 复杂表达式
    @PreAuthorize("#oauth2.clientAuthorities.contains('SCOPE_message:read')")
    public Message getMessage(Long messageId) {
        return messageRepository.findById(messageId)
            .orElseThrow(() -> new MessageNotFoundException(messageId));
    }

    // 在方法执行后进行权限检查
    @PostAuthorize("returnObject.owner == authentication.principal.username or hasRole('ADMIN')")
    public User getUserProfile(Long userId) {
        return userRepository.findById(userId)
            .orElseThrow(() -> new UserNotFoundException(userId));
    }

    // 过滤输入参数
    @PreFilter("filterObject.owner == authentication.principal.username")
    public void deleteMessages(List<Message> messages) {
        messageRepository.deleteAll(messages);
    }

    // 过滤返回结果
    @PostFilter("filterObject.owner == authentication.principal.username")
    public List<Message> getMyMessages() {
        return messageRepository.findAll();
    }
}

六、会话管理

6.1 会话创建策略

java 复制代码
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .sessionManagement(session -> session
                // 会话创建策略
                .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
                
                // 并发会话控制
                .maximumSessions(1)
                .maxSessionsPreventsLogin(false)  // 踢掉之前登录
                
                // 会话过期处理
                .expiredUrl("/session-expired")
                
                // 无效会话处理
                .invalidSessionUrl("/session-invalid"));
        
        return http.build();
    }
}

SessionCreationPolicy 选项:

策略 说明 适用场景
ALWAYS 总是创建会话 需要会话管理
IF_REQUIRED 按需创建(默认) 标准 Web 应用
NEVER 不会主动创建,已有则使用 API 服务
STATELESS 从不创建会话 JWT、无状态 API

6.2 会话固定攻击防护

java 复制代码
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .sessionManagement(session -> session
                // 会话 fixation 保护
                .sessionFixation(sessionFixation -> sessionFixation
                    // 登录时创建新会话,旧会话数据迁移
                    .migrateSession()
                    // 或者使用 none/changeSessionId
                    // .none()
                    // .changeSessionId()
                ));
        
        return http.build();
    }
}

会话固定攻击防护选项:

选项 说明 安全性
migrateSession 登录后创建新会话,复制属性 推荐
changeSessionId 保持会话 ID,修改值 兼容性
newSession 创建全新会话 最高安全

6.3 集群会话管理

java 复制代码
@Configuration
public class SessionConfig {

    @Bean
    public SpringSessionBackedSessionRegistry sessionRegistry(
            FindByIndexNameSessionRepository<?> sessionRepository) {
        return new SpringSessionBackedSessionRegistry(sessionRepository);
    }
}

Redis 会话配置:

java 复制代码
@Configuration
@EnableRedisHttpSession(
    maxInactiveIntervalInSeconds = 3600,  // 1小时
    redisNamespace = "spring:session",
    redisFlushMode = RedisFlushMode.ON_SAVE
)
public class RedisSessionConfig {
    
    @Bean
    public RedisIndexedSessionRepository sessionRepository(
            RedisConnectionFactory connectionFactory) {
        return new RedisIndexedSessionRepository(
            new RedisOperationsSessionRepository(connectionFactory));
    }
}

七、密码加密

7.1 PasswordEncoder 接口

java 复制代码
public interface PasswordEncoder {
    
    /**
     * 加密密码
     */
    String encode(CharSequence rawPassword);
    
    /**
     * 验证密码是否匹配
     */
    boolean matches(CharSequence rawPassword, String encodedPassword);
    
    /**
     * 判断是否需要重新编码
     */
    default boolean upgradeEncoding(String encodedPassword) {
        return false;
    }
}

7.2 常用密码加密器对比

加密器 说明 安全性 性能
NoOpPasswordEncoder 不加密(仅测试) -
PlaintextPasswordEncoder 明文存储 -
BCryptPasswordEncoder BCrypt 算法 较慢
Argon2PasswordEncoder Argon2 算法 ✅✅
SCryptPasswordEncoder SCrypt 算法 ✅✅
Pbkdf2PasswordEncoder PBKDF2 算法 ✅✅

7.3 密码编码配置

java 复制代码
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public PasswordEncoder passwordEncoder() {
        // 1. BCrypt(推荐)
        return new BCryptPasswordEncoder();
        
        // 2. Argon2(更安全,但慢)
        return Argon2PasswordEncoder.defaultsForSpringSecurity_5_8();
        
        // 3. 委托链(自动选择最优)
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }
}

7.4 DelegatingPasswordEncoder

Spring Security 推荐使用委托密码编码器,它会自动处理不同格式的密码:

java 复制代码
@Bean
public PasswordEncoder passwordEncoder() {
    return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}

// 生成的密码格式:{编码器ID}encodedPassword
// 例如:{bcrypt}$2a$10$...
// 例如:{sha256}XXXX

自定义委托编码器:

java 复制代码
@Bean
public PasswordEncoder passwordEncoder() {
    Map<String, PasswordEncoder> encoders = new HashMap<>();
    encoders.put("bcrypt", new BCryptPasswordEncoder(12));
    encoders.put("argon2", Argon2PasswordEncoder.defaultsForSpringSecurity_5_8());
    encoders.put("ldap", new org.springframework.security.crypto.password.
        LdapPasswordEncoder());
    
    return new DelegatingPasswordEncoder("bcrypt", encoders);
}

八、CORS 跨域配置

8.1 全局 CORS 配置

java 复制代码
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            // 1. 禁用 CSRF(API 场景)
            .csrf(csrf -> csrf.disable())
            
            // 2. 配置 CORS
            .cors(cors -> cors.configurationSource(corsConfigurationSource()));
        
        return http.build();
    }
    
    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOriginPatterns(
            List.of("http://localhost:3000", "https://*.example.com"));
        configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
        configuration.setAllowedHeaders(List.of("*"));
        configuration.setAllowCredentials(true);
        configuration.setMaxAge(3600L);
        
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }
}

8.2 跨域与安全配置结合

java 复制代码
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            // CORS 配置
            .cors(cors -> cors.configurationSource(corsConfigurationSource()))
            
            // 允许跨域请求携带认证信息
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/**").permitAll()
                .anyRequest().authenticated())
            
            // 如果需要 Cookie,设置 SameSite
            .csrf(csrf -> csrf
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
                .csrfTokenRequestAttribute("_csrf"));
        
        return http.build();
    }
}

九、CSRF 防护

9.1 CSRF 攻击原理

转账API 恶意网站 银行网站 用户 转账API 恶意网站 银行网站 用户 自动提交隐藏表单 携带银行 Cookie 登录网银 返回认证 Cookie 访问恶意网站 返回含恶意表单的页面 POST /transfer?to=黑客&amount=10000 扣款请求 执行转账 转账成功

9.2 CSRF 防护配置

java 复制代码
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            // Cookie 保存 CSRF Token
            .csrf(csrf -> csrf
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
                .csrfRequestAttribute("_csrf"))
            
            // 忽略特定路径
            .ignoringRequestMatchers("/api/**", "/webhook/**");
        
        return http.build();
    }
}

9.3 前端 CSRF Token 处理

javascript 复制代码
// 1. 从 Cookie 获取 CSRF Token(Spring Security 默认行为)
function getCsrfToken() {
    const cookies = document.cookie.split(';');
    for (const cookie of cookies) {
        const [name, value] = cookie.trim().split('=');
        if (name === 'XSRF-TOKEN') {
            return value;
        }
    }
    return null;
}

// 2. 添加到请求头
function fetchWithCsrf(url, options = {}) {
    const csrfToken = getCsrfToken();
    return fetch(url, {
        ...options,
        headers: {
            ...options.headers,
            'X-XSRF-TOKEN': csrfToken
        }
    });
}

// 3. Axios 配置
const axiosInstance = axios.create({
    baseURL: '/api'
});

axiosInstance.interceptors.request.use(config => {
    const csrfToken = getCsrfToken();
    if (csrfToken) {
        config.headers['X-XSRF-TOKEN'] = csrfToken;
    }
    return config;
});

十、安全响应头

10.1 常用安全响应头

java 复制代码
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            // 安全响应头
            .securityHeaders(headers -> headers
                // 防止点击劫持
                .frameOptions(frame -> frame.deny())
                
                // XSS 防护
                .contentSecurityPolicy(csp -> csp
                    .policyDirectives("default-src 'self'; script-src 'self' 'unsafe-inline';"))
                
                // 防止 MIME 类型嗅探
                .contentTypeOptions(contentType -> {})
                
                // Strict Transport Security
                .httpStrictTransportSecurity(hsts -> hsts
                    .includeSubDomains(true)
                    .maxAgeInSeconds(31536000))
                
                // 权限策略
                .permissionsPolicy(policy -> policy
                    .addPolicyDirectives("geolocation=(), camera=(), microphone=()")));
        
        return http.build();
    }
}

10.2 常用安全头说明

响应头 说明 值示例
X-Frame-Options 防止页面被 iframe 嵌入 DENY / SAMEORIGIN
X-Content-Type-Options 防止 MIME 嗅探 nosniff
X-XSS-Protection XSS 过滤器(已废弃,依赖 CSP) 1; mode=block
Strict-Transport-Security 强制 HTTPS max-age=31536000
Content-Security-Policy 内容安全策略 default-src 'self'
Permissions-Policy 权限策略 camera=(), microphone=()
Referrer-Policy 来源策略 strict-origin-when-cross-origin

10.3 Content Security Policy 配置

java 复制代码
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .securityHeaders(headers -> headers
                .contentSecurityPolicy(csp -> csp
                    .policyDirectives(
                        // 默认来源:仅同源
                        "default-src 'self' " +
                        // 脚本:同源 + 内联(开发环境)
                        "script-src 'self' 'unsafe-inline' " +
                        // 图片:同源 + CDN
                        "img-src 'self' https://cdn.example.com data: " +
                        // 样式:同源 + 内联
                        "style-src 'self' 'unsafe-inline' " +
                        // 连接:仅同源
                        "connect-src 'self' " +
                        // 字体:同源
                        "font-src 'self' " +
                        // frame 来源
                        "frame-src 'none' " +
                        // object 来源
                        "object-src 'none'"
                    )
                ));
        
        return http.build();
    }
}

十一、Remember-Me 功能

java 复制代码
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .rememberMe(rememberMe -> rememberMe
                // Cookie 名
                .rememberMeCookie("remember-me")
                // Token 有效期(秒)
                .tokenValiditySeconds(86400 * 14)  // 14 天
                // 记住我参数名
                .rememberMeParameter("remember-me")
                // 用户名参数(用于验证)
                .userDetailsService(userDetailsService())
                // Key 用于签名
                .key("my-secret-key"));
        
        return http.build();
    }
}

11.2 基于持久化的 Remember-Me

java 复制代码
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Autowired
    private DataSource dataSource;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .rememberMe(rememberMe -> rememberMe
                .tokenRepository(persistentTokenRepository())
                .tokenValiditySeconds(86400 * 14)
                .userDetailsService(userDetailsService()));
        
        return http.build();
    }
    
    @Bean
    public PersistentTokenRepository persistentTokenRepository() {
        JdbcTokenRepositoryImpl repository = new JdbcTokenRepositoryImpl();
        repository.setDataSource(dataSource);
        // 首次使用自动创建表
        // repository.setCreateTableOnStartup(true);
        return repository;
    }
}

持久化表结构:

sql 复制代码
CREATE TABLE persistent_logins (
    username VARCHAR(64) NOT NULL,
    series VARCHAR(64) PRIMARY KEY,
    token VARCHAR(64) NOT NULL,
    last_used TIMESTAMP NOT NULL
);

十二、异常处理

12.1 认证异常处理

java 复制代码
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .exceptionHandling(exceptions -> exceptions
                // 认证失败处理
                .authenticationEntryPoint((request, response, authException) -> {
                    response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                    response.setContentType("application/json");
                    response.getWriter().write(
                        "{\"error\": \"Unauthorized\", \"message\": \"" + 
                        authException.getMessage() + "\"}");
                })
                
                // 授权失败处理
                .accessDeniedHandler((request, response, accessDeniedException) -> {
                    response.setStatus(HttpServletResponse.SC_FORBIDDEN);
                    response.setContentType("application/json");
                    response.getWriter().write(
                        "{\"error\": \"Forbidden\", \"message\": \"" + 
                        accessDeniedException.getMessage() + "\"}");
                }));
        
        return http.build();
    }
}

12.2 自定义异常页面

java 复制代码
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .exceptionHandling(exceptions -> exceptions
                .authenticationEntryPoint(new CustomAuthenticationEntryPoint())
                .accessDeniedHandler(new CustomAccessDeniedHandler()))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/error/**").permitAll()
                .anyRequest().authenticated());
        
        return http.build();
    }
}

public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
    
    @Override
    public void commence(HttpServletRequest request, 
                        HttpServletResponse response,
                        AuthenticationException authException) throws IOException {
        
        String accept = request.getHeader("Accept");
        
        if (accept != null && accept.contains("application/json")) {
            // API 请求返回 JSON
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.setContentType("application/json");
            response.getWriter().write(
                JSON.toJSONString(ApiResult.unauthorized(authException.getMessage())));
        } else {
            // 页面请求重定向到登录页
            response.sendRedirect("/login?unauthorized=true");
        }
    }
}

十三、测试安全配置

13.1 使用 @WithMockUser 测试

java 复制代码
@SpringBootTest
@AutoConfigureMockMvc
class SecurityIntegrationTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    @WithMockUser(roles = "USER")
    void userCanAccessUserEndpoint() throws Exception {
        mockMvc.perform(get("/api/user"))
            .andExpect(status().isOk());
    }

    @Test
    @WithMockUser(roles = "USER")
    void userCannotAccessAdminEndpoint() throws Exception {
        mockMvc.perform(get("/api/admin"))
            .andExpect(status().isForbidden());
    }

    @Test
    @WithMockUser(username = "admin", roles = {"ADMIN", "USER"})
    void adminCanAccessAdminEndpoint() throws Exception {
        mockMvc.perform(get("/api/admin"))
            .andExpect(status().isOk());
    }

    @Test
    void anonymousCannotAccessProtectedEndpoint() throws Exception {
        mockMvc.perform(get("/api/user"))
            .andExpect(status().isUnauthorized());
    }
}

13.2 使用 @WithUserDetails 测试

java 复制代码
@SpringBootTest
@AutoConfigureMockMvc
class UserDetailsIntegrationTest {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private UserDetailsService userDetailsService;

    @Test
    @WithUserDetails("testuser")
    void userCanAccessOwnData() throws Exception {
        mockMvc.perform(get("/api/user/profile"))
            .andExpect(status().isOk());
    }

    @Test
    @WithUserDetails("admin")
    void adminCanAccessAllData() throws Exception {
        mockMvc.perform(get("/api/admin/users"))
            .andExpect(status().isOk());
    }
}

13.3 SecurityTest 注解

java 复制代码
@SpringBootTest
@AutoConfigureMockMvc
class MethodSecurityTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    @WithMockUser
    void testMethodSecurity() throws Exception {
        mockMvc.perform(get("/api/admin/method"))
            .andExpect(status().isOk());
    }

    @Test
    @WithSecurityContext(factory = CustomSecurityContextFactory.class)
    void testCustomSecurityContext() throws Exception {
        // 自定义安全上下文测试
        mockMvc.perform(get("/api/special"))
            .andExpect(status().isOk());
    }
}

public class CustomSecurityContextFactory 
        implements WithSecurityContextFactory<WithSecurityContext> {
    
    @Override
    public SecurityContext createSecurityContext(WithSecurityContext annotation) {
        SecurityContext context = SecurityContextHolder.createEmptyContext();
        
        CustomUserDetails user = new CustomUserDetails();
        user.setUsername("custom-user");
        user.addAuthority(new SimpleGrantedAuthority("CUSTOM_ROLE"));
        
        Authentication auth = new UsernamePasswordAuthenticationToken(
            user, "password", user.getAuthorities());
        context.setAuthentication(auth);
        
        return context;
    }
}

十四、总结

14.1 核心要点

  1. 从 WebSecurityConfigurerAdapter 迁移到 @Bean SecurityFilterChain
  2. 使用 Lambda DSL 进行配置(更简洁、更易读)
  3. authorizeHttpRequests 替代 authorizeRequests
  4. requestMatchers 替代 antMatchers
  5. hasRole() 自动添加 ROLE_ 前缀

14.2 配置检查清单

java 复制代码
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            // 1. 路径配置
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/public/**").permitAll()
                .anyRequest().authenticated())
            
            // 2. 认证配置(表单/OAuth2/JWT)
            .formLogin(form -> form
                .loginPage("/login")
                .permitAll())
            
            // 3. 登出配置
            .logout(logout -> logout
                .logoutUrl("/logout"))
            
            // 4. CSRF 配置
            .csrf(csrf -> csrf
                .ignoringRequestMatchers("/api/**"))
            
            // 5. CORS 配置
            .cors(cors -> cors.configurationSource(corsConfigurationSource()))
            
            // 6. 会话管理
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED))
            
            // 7. 异常处理
            .exceptionHandling(exceptions -> exceptions
                .authenticationEntryPoint(authenticationEntryPoint())
                .accessDeniedHandler(accessDeniedHandler()));
        
        return http.build();
    }
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }
}

14.3 迁移检查表

检查项 说明
WebSecurityConfigurerAdapter 改用 @Bean SecurityFilterChain
configure(HttpSecurity) 改用 Lambda DSL
antMatchers() 改用 requestMatchers()
authorizeRequests() 改用 authorizeHttpRequests()
hasRole("ADMIN") 检查是否需要添加 ROLE_ 前缀
csrf().disable() 改用 csrf(csrf -> csrf.disable())
httpBasic() 改用 httpBasic(basic -> {})
formLogin() 改用 formLogin(form -> form.xxx)
PasswordEncoder 使用 PasswordEncoderFactories 创建

本文深入解析了 Spring Security 6.x 的核心配置、Lambda DSL、认证授权机制、安全响应头等内容。结合实际项目需求,灵活运用这些配置,可以构建安全可靠的 Web 应用。

相关推荐
全栈小刘1 小时前
Claude Code 为什么这么顺?Anthropic 最新复盘:真正撑住它的不是模型,而是缓存
后端
BING_Algorithm1 小时前
一文搞定 AOP 所有核心知识点
spring boot·后端·spring
善恶怪客1 小时前
Java-数据类型
java
学习3人组2 小时前
Mes全连路架构图
java·erp
上弦月-编程2 小时前
C语言指针从入门到实战
java·jvm·算法
Cyan_RA92 小时前
SpringMVC 请求数据绑定与参数映射 详解
java·后端·spring·mvc·springmvc·映射请求数据
逻辑驱动的ken2 小时前
Java高频面试考点场景题20
java·开发语言·深度学习·面试·职场和发展
GISer_Jing2 小时前
AI全栈工程师知识体系全景:从前后端核心架构到落地项目全拆解
前端·人工智能·后端·ai编程
bzmK1DTbd2 小时前
Java游戏服务器:Netty框架的高并发网络通信
java·服务器·游戏