| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117 |
- package com.xuekairui.user.config;
- import com.fasterxml.jackson.databind.ObjectMapper;
- import com.xuekairui.common.ErrorCode;
- import com.xuekairui.common.Result;
- import com.xuekairui.user.security.JwtAuthenticationFilter;
- import lombok.RequiredArgsConstructor;
- import org.springframework.context.annotation.Bean;
- import org.springframework.context.annotation.Configuration;
- import org.springframework.core.env.Environment;
- import org.springframework.http.HttpMethod;
- import org.springframework.http.HttpStatus;
- import org.springframework.http.MediaType;
- 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.configurers.AbstractHttpConfigurer;
- import org.springframework.security.config.http.SessionCreationPolicy;
- import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
- import org.springframework.security.crypto.password.PasswordEncoder;
- import org.springframework.security.web.SecurityFilterChain;
- import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
- import java.nio.charset.StandardCharsets;
- /**
- * Spring Security 配置
- */
- @Configuration
- @EnableWebSecurity
- @RequiredArgsConstructor
- public class SecurityConfig {
- private final JwtAuthenticationFilter jwtAuthenticationFilter;
- private final Environment environment;
- /** 公开接口白名单 */
- private static final String[] PUBLIC_URLS = {
- "/api/auth/sms/send", // 发送验证码
- "/api/auth/sms/login", // 短信登录
- "/api/auth/password/login", // 密码登录
- "/api/auth/refresh", // 刷新Token(无需认证)
- "/api/auth/wechat/**", // 微信登录相关
- "/api/invite/validate", // 验证邀请码(注册前)
- "/api/invite/{code}", // 邀请链接入口(公开)
- "/api/invite/page/**", // 邀请落地页(公开)
- "/api/invite/click/**", // 邀请链接点击追踪(公开)
- "/api/invite/resolve-link", // 解析邀请链接/邀请码(公开)
- "/invite/**", // 浏览器直接访问邀请链接(公开)
- "/api/platform-config/**", // 平台配置查询(公开)
- "/api/search/suggestions", // 搜索建议(公开)
- "/api/payment/callback/**", // 支付回调(公开,供微信/支付宝调用)
- "/api/payment/anxin/notify/**", // 安心付通知(公开,供支付宝调用)
- "/api/payment/agreement/notify/**", // 签约回调通知(公开,供微信调用)
- "/api/payment/alipay/**", // 支付宝开放能力回调(公开)
- "/api/payment/plans", // 支付方案列表(公开)
- "/api/membership/levels", // 会员等级列表(公开)
- "/api/membership/levels/*", // 会员等级详情(公开)
- "/error", // 错误页面
- "/actuator/health" // 健康检查
- };
- @Bean
- public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
- // dev 环境启用后端 CORS 兜底(DevCorsConfig 提供配置源),生产环境由 Nginx 统一处理
- boolean isDev = environment.acceptsProfiles(org.springframework.core.env.Profiles.of("dev"));
- if (isDev) {
- http.cors(cors -> {}); // 使用默认,自动发现 CorsConfigurationSource Bean
- } else {
- http.cors(AbstractHttpConfigurer::disable);
- }
- http
- .csrf(AbstractHttpConfigurer::disable)
- .sessionManagement(session ->
- session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
- .authorizeHttpRequests(auth -> auth
- // OPTIONS 预检请求直接放行(CORS 由 Nginx 处理,此处仅作兜底避免 401)
- .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
- // 超级管理员专属:角色赋权
- .requestMatchers("/api/admin/users/**").hasRole("SUPER_ADMIN")
- // 运营后台接口
- .requestMatchers("/api/admin/**").hasAnyRole("ADMIN", "SUPER_ADMIN")
- // 散落在用户路径中的管理员操作(已统一到 /api/admin/** 下)
- .requestMatchers("/api/coupon/issue").hasAnyRole("ADMIN", "SUPER_ADMIN")
- // 用户端邀请接口需登录(必须在 /api/invite/{code} 公开匹配之前)
- .requestMatchers("/api/invite/code", "/api/invite/stats",
- "/api/invite/rewards", "/api/invite/extra-quota",
- "/api/invite/bind").authenticated()
- .requestMatchers(PUBLIC_URLS).permitAll()
- .anyRequest().authenticated()
- )
- .exceptionHandling(ex -> ex
- .authenticationEntryPoint((request, response, authException) -> {
- response.setStatus(HttpStatus.UNAUTHORIZED.value());
- response.setContentType(MediaType.APPLICATION_JSON_VALUE);
- response.setCharacterEncoding(StandardCharsets.UTF_8.name());
- Result<Void> result = Result.error(ErrorCode.UNAUTHORIZED);
- response.getWriter().write(new ObjectMapper().writeValueAsString(result));
- })
- .accessDeniedHandler((request, response, accessDeniedException) -> {
- response.setStatus(HttpStatus.FORBIDDEN.value());
- response.setContentType(MediaType.APPLICATION_JSON_VALUE);
- response.setCharacterEncoding(StandardCharsets.UTF_8.name());
- Result<Void> result = Result.error(ErrorCode.FORBIDDEN);
- response.getWriter().write(new ObjectMapper().writeValueAsString(result));
- })
- )
- .addFilterBefore(jwtAuthenticationFilter,
- UsernamePasswordAuthenticationFilter.class);
- return http.build();
- }
- @Bean
- public PasswordEncoder passwordEncoder() {
- return new BCryptPasswordEncoder();
- }
- }
|