package com.youlai.boot.config; import cn.binarywang.wx.miniapp.api.WxMaService; import cn.hutool.core.util.ArrayUtil; import com.youlai.boot.framework.security.filter.MobileApiSignatureFilter; import com.youlai.boot.auth.service.CaptchaService; import com.youlai.boot.config.property.SecurityProperties; import com.youlai.boot.framework.security.filter.TokenAuthenticationFilter; import com.youlai.boot.framework.security.port.UserAuthenticationPort; import com.youlai.boot.framework.security.service.SecurityUserDetailsService; import com.youlai.boot.framework.security.token.TokenManager; import com.youlai.boot.auth.security.filter.CaptchaValidationFilter; import com.youlai.boot.auth.security.handler.JsonAccessDeniedHandler; import com.youlai.boot.auth.security.handler.JsonAuthenticationEntryPoint; import com.youlai.boot.auth.security.provider.SmsAuthenticationProvider; import com.youlai.boot.auth.security.provider.WxMaAuthenticationProvider; import com.youlai.boot.system.service.UserSocialService; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; 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; import org.springframework.security.web.access.intercept.AuthorizationFilter; import jakarta.servlet.DispatcherType; /** * Spring Security 配置类。 *

* 归使用方(auth 模块),安全规则(放行路径、CORS、Provider 装配、响应格式) * 因项目而异,不应由框架层强制装配。 * * @author Ray.Hao * @since 4.3.1 */ @Configuration @EnableWebSecurity @EnableMethodSecurity @RequiredArgsConstructor public class SecurityConfig { private final RedisTemplate redisTemplate; private final PasswordEncoder passwordEncoder; private final TokenManager tokenManager; private final SecurityUserDetailsService userDetailsService; private final CaptchaService captchaService; private final SecurityProperties securityProperties; @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { return http .authorizeHttpRequests(requestMatcherRegistry -> { // ASYNC 分发(SSE 超时/完成后的异步 dispatch)无 Authorization 头,直接放行 requestMatcherRegistry.dispatcherTypeMatchers(DispatcherType.ASYNC).permitAll(); String[] ignoreUrls = securityProperties.getIgnoreUrls(); if (ArrayUtil.isNotEmpty(ignoreUrls)) { requestMatcherRegistry.requestMatchers(ignoreUrls).permitAll(); } // 移动设备专用接口路径(需要设备签名验证,但不需要用户登录) requestMatcherRegistry.requestMatchers("/api/v1/sn/**").permitAll(); requestMatcherRegistry.requestMatchers("/api/v1/app/**").permitAll(); // 其他所有请求需登录后访问 requestMatcherRegistry.anyRequest().authenticated(); } ) .exceptionHandling(configurer -> configurer .authenticationEntryPoint(new JsonAuthenticationEntryPoint()) .accessDeniedHandler(new JsonAccessDeniedHandler()) ) .sessionManagement(configurer -> configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS) ) .csrf(AbstractHttpConfigurer::disable) .formLogin(AbstractHttpConfigurer::disable) .httpBasic(AbstractHttpConfigurer::disable) .headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable)) // 验证码校验(使用方过滤器,直接写 JSON 响应) .addFilterBefore(new CaptchaValidationFilter(captchaService), UsernamePasswordAuthenticationFilter.class) // Token 认证(Starter 过滤器,抛 AuthenticationException 交给 ExceptionTranslationFilter 处理) .addFilterBefore(new TokenAuthenticationFilter(tokenManager), AuthorizationFilter.class) // 移动设备API签名验证过滤器(仅对 /api/v1/sn/** 路径生效) .addFilterBefore(new MobileApiSignatureFilter(redisTemplate), UsernamePasswordAuthenticationFilter.class) .build(); } @Bean public WebSecurityCustomizer webSecurityCustomizer() { return (web) -> { String[] unsecuredUrls = securityProperties.getUnsecuredUrls(); if (ArrayUtil.isNotEmpty(unsecuredUrls)) { web.ignoring().requestMatchers(unsecuredUrls); } }; } @Bean public DaoAuthenticationProvider daoAuthenticationProvider() { DaoAuthenticationProvider provider = new DaoAuthenticationProvider(userDetailsService); provider.setPasswordEncoder(passwordEncoder); return provider; } @Bean public SmsAuthenticationProvider smsAuthenticationProvider(UserAuthenticationPort userAuthPort) { return new SmsAuthenticationProvider(userAuthPort, redisTemplate); } @Bean public WxMaAuthenticationProvider wechatMiniAuthenticationProvider( WxMaService wxMaService, UserAuthenticationPort userAuthenticationPort, UserSocialService userSocialService ) { return new WxMaAuthenticationProvider(wxMaService, userAuthenticationPort, userSocialService); } @Bean public AuthenticationManager authenticationManager( DaoAuthenticationProvider daoAuthenticationProvider, SmsAuthenticationProvider smsAuthenticationProvider, WxMaAuthenticationProvider wxMaAuthenticationProvider ) { return new ProviderManager( daoAuthenticationProvider, smsAuthenticationProvider, wxMaAuthenticationProvider ); } }