package com.zsElectric.boot.config; import cn.binarywang.wx.miniapp.api.WxMaService; import cn.hutool.captcha.generator.CodeGenerator; import cn.hutool.core.util.ArrayUtil; import com.zsElectric.boot.common.util.electric.queryToken.ThirdPartyJwtAuthFilter; import com.zsElectric.boot.config.property.SecurityProperties; import com.zsElectric.boot.core.filter.RateLimiterFilter; import com.zsElectric.boot.security.filter.CaptchaValidationFilter; import com.zsElectric.boot.security.filter.TokenAuthenticationFilter; import com.zsElectric.boot.security.handler.MyAccessDeniedHandler; import com.zsElectric.boot.security.handler.MyAuthenticationEntryPoint; import com.zsElectric.boot.security.provider.SmsAuthenticationProvider; import com.zsElectric.boot.security.provider.WxMiniAppCodeAuthenticationProvider; import com.zsElectric.boot.security.provider.WxMiniAppPhoneAuthenticationProvider; import com.zsElectric.boot.security.provider.WxMiniAppPhoneCodeAuthenticationProvider; import com.zsElectric.boot.security.token.TokenManager; import com.zsElectric.boot.security.service.SysUserDetailsService; import com.zsElectric.boot.system.service.ConfigService; import com.zsElectric.boot.system.service.UserService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.ProviderManager; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; /** * Spring Security 配置类 * * @author Ray.Hao * @since 2023/2/17 */ @Slf4j @Configuration @EnableWebSecurity @EnableMethodSecurity @RequiredArgsConstructor public class SecurityConfig { private final RedisTemplate redisTemplate; private final PasswordEncoder passwordEncoder; private final TokenManager tokenManager; private final WxMaService wxMaService; private final UserService userService; private final SysUserDetailsService userDetailsService; private final CodeGenerator codeGenerator; private final ConfigService configService; private final SecurityProperties securityProperties; private final ThirdPartyJwtAuthFilter thirdPartyAuthFilter; // 注入 UserInfoService 用于小程序认证 private final com.zsElectric.boot.business.service.UserInfoService userInfoService; // 仅针对第三方URL的安全过滤链,只挂第三方认证过滤器 @Bean @Order(1) public SecurityFilterChain thirdPartySecurityFilterChain(HttpSecurity http) throws Exception { log.info("第三方认证过滤器: {}", thirdPartyAuthFilter); String[] thirdPartyUrls = securityProperties.getThirdPartyUrls(); if (thirdPartyUrls == null || thirdPartyUrls.length == 0) { log.warn("第三方URL未配置或为空,使用占位符避免匹配所有请求"); thirdPartyUrls = new String[]{"/__thirdparty_noop__"}; } log.info("========== 配置第三方 SecurityFilterChain, urls: {} ==========", (Object) thirdPartyUrls); http .securityMatcher(thirdPartyUrls) // 只匹配第三方URL .authorizeHttpRequests(registry -> registry .anyRequest().authenticated() ) .exceptionHandling(configurer -> configurer .authenticationEntryPoint(new MyAuthenticationEntryPoint()) .accessDeniedHandler(new MyAccessDeniedHandler()) ) .sessionManagement(configurer -> configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS) ) .csrf(AbstractHttpConfigurer::disable) .formLogin(AbstractHttpConfigurer::disable) .httpBasic(AbstractHttpConfigurer::disable) .headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable)) // 只加第三方认证过滤器 .addFilterBefore(thirdPartyAuthFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } /** * 配置安全过滤链 SecurityFilterChain */ @Bean @Order(2) public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { log.info("========== 配置 SecurityFilterChain =========="); return http .authorizeHttpRequests(requestMatcherRegistry -> { // 配置无需登录即可访问的公开接口 String[] ignoreUrls = securityProperties.getIgnoreUrls(); log.info("安全白名单 ignore-urls: {}", (Object) ignoreUrls); if (ArrayUtil.isNotEmpty(ignoreUrls)) { requestMatcherRegistry.requestMatchers(ignoreUrls).permitAll(); } // 配置完全绕过安全检查的路径(原 unsecuredUrls) String[] unsecuredUrls = securityProperties.getUnsecuredUrls(); log.info("非安全端点 unsecured-urls: {}", (Object) unsecuredUrls); if (ArrayUtil.isNotEmpty(unsecuredUrls)) { requestMatcherRegistry.requestMatchers(unsecuredUrls).permitAll(); } // 其他所有请求需登录后访问 requestMatcherRegistry.anyRequest().authenticated(); } ) .exceptionHandling(configurer -> configurer .authenticationEntryPoint(new MyAuthenticationEntryPoint()) // 未认证异常处理器 .accessDeniedHandler(new MyAccessDeniedHandler()) // 无权限访问异常处理器 ) // 禁用默认的 Spring Security 特性,适用于前后端分离架构 .sessionManagement(configurer -> configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 无状态认证,不使用 Session ) .csrf(AbstractHttpConfigurer::disable) // 禁用 CSRF 防护,前后端分离无需此防护机制 .formLogin(AbstractHttpConfigurer::disable) // 禁用默认的表单登录功能,前后端分离采用 Token 认证方式 .httpBasic(AbstractHttpConfigurer::disable) // 禁用 HTTP Basic 认证,避免弹窗式登录 // 禁用 X-Frame-Options 响应头,允许页面被嵌套到 iframe 中 .headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable)) // 限流过滤器 .addFilterBefore(new RateLimiterFilter(redisTemplate, configService), UsernamePasswordAuthenticationFilter.class) // 验证码校验过滤器 .addFilterBefore(new CaptchaValidationFilter(redisTemplate, codeGenerator), UsernamePasswordAuthenticationFilter.class) // 验证和解析过滤器 .addFilterBefore(new TokenAuthenticationFilter(tokenManager), UsernamePasswordAuthenticationFilter.class) .build(); } /** * 配置Web安全自定义器,以忽略特定请求路径的安全性检查。 *

* 该配置用于指定哪些请求路径不经过Spring Security过滤器链。通常用于静态资源文件。 * 注意:这些警告可以忽略,因为Swagger等静态资源需要完全绕过过滤器链才能正常访问。 */ @Bean public WebSecurityCustomizer webSecurityCustomizer() { return (web) -> { String[] unsecuredUrls = securityProperties.getUnsecuredUrls(); if (ArrayUtil.isNotEmpty(unsecuredUrls)) { web.ignoring().requestMatchers(unsecuredUrls); } }; } /** * 默认密码认证的 Provider */ @Bean public DaoAuthenticationProvider daoAuthenticationProvider() { DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider(userDetailsService); daoAuthenticationProvider.setPasswordEncoder(passwordEncoder); return daoAuthenticationProvider; } /** * 微信小程序Code认证Provider */ @Bean public WxMiniAppCodeAuthenticationProvider wxMiniAppCodeAuthenticationProvider() { return new WxMiniAppCodeAuthenticationProvider(userService, wxMaService); } /** * 微信小程序手机号认证Provider */ @Bean public WxMiniAppPhoneAuthenticationProvider wxMiniAppPhoneAuthenticationProvider() { return new WxMiniAppPhoneAuthenticationProvider(userService, wxMaService); } /** * 微信小程序手机号Code认证Provider(新版接口) *

* 使用 UserInfoService 直接操作 c_user_info 表 */ @Bean public WxMiniAppPhoneCodeAuthenticationProvider wxMiniAppPhoneCodeAuthenticationProvider() { return new WxMiniAppPhoneCodeAuthenticationProvider(userInfoService, wxMaService); } /** * 短信验证码认证 Provider */ @Bean public SmsAuthenticationProvider smsAuthenticationProvider() { return new SmsAuthenticationProvider(userService, redisTemplate); } /** * 认证管理器 */ @Bean public AuthenticationManager authenticationManager( DaoAuthenticationProvider daoAuthenticationProvider, WxMiniAppCodeAuthenticationProvider wxMiniAppCodeAuthenticationProvider, WxMiniAppPhoneAuthenticationProvider wxMiniAppPhoneAuthenticationProvider, WxMiniAppPhoneCodeAuthenticationProvider wxMiniAppPhoneCodeAuthenticationProvider, SmsAuthenticationProvider smsAuthenticationProvider ) { return new ProviderManager( daoAuthenticationProvider, wxMiniAppCodeAuthenticationProvider, wxMiniAppPhoneAuthenticationProvider, wxMiniAppPhoneCodeAuthenticationProvider, smsAuthenticationProvider ); } }