SecurityConfig.java 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  1. package com.xuekairui.user.config;
  2. import com.fasterxml.jackson.databind.ObjectMapper;
  3. import com.xuekairui.common.ErrorCode;
  4. import com.xuekairui.common.Result;
  5. import com.xuekairui.user.security.JwtAuthenticationFilter;
  6. import lombok.RequiredArgsConstructor;
  7. import org.springframework.context.annotation.Bean;
  8. import org.springframework.context.annotation.Configuration;
  9. import org.springframework.core.env.Environment;
  10. import org.springframework.http.HttpMethod;
  11. import org.springframework.http.HttpStatus;
  12. import org.springframework.http.MediaType;
  13. import org.springframework.security.config.annotation.web.builders.HttpSecurity;
  14. import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
  15. import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
  16. import org.springframework.security.config.http.SessionCreationPolicy;
  17. import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
  18. import org.springframework.security.crypto.password.PasswordEncoder;
  19. import org.springframework.security.web.SecurityFilterChain;
  20. import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
  21. import java.nio.charset.StandardCharsets;
  22. /**
  23. * Spring Security 配置
  24. */
  25. @Configuration
  26. @EnableWebSecurity
  27. @RequiredArgsConstructor
  28. public class SecurityConfig {
  29. private final JwtAuthenticationFilter jwtAuthenticationFilter;
  30. private final Environment environment;
  31. /** 公开接口白名单 */
  32. private static final String[] PUBLIC_URLS = {
  33. "/api/auth/sms/send", // 发送验证码
  34. "/api/auth/sms/login", // 短信登录
  35. "/api/auth/password/login", // 密码登录
  36. "/api/auth/refresh", // 刷新Token(无需认证)
  37. "/api/auth/wechat/**", // 微信登录相关
  38. "/api/invite/validate", // 验证邀请码(注册前)
  39. "/api/invite/{code}", // 邀请链接入口(公开)
  40. "/api/invite/page/**", // 邀请落地页(公开)
  41. "/api/invite/click/**", // 邀请链接点击追踪(公开)
  42. "/api/invite/resolve-link", // 解析邀请链接/邀请码(公开)
  43. "/invite/**", // 浏览器直接访问邀请链接(公开)
  44. "/api/platform-config/**", // 平台配置查询(公开)
  45. "/api/search/suggestions", // 搜索建议(公开)
  46. "/api/payment/callback/**", // 支付回调(公开,供微信/支付宝调用)
  47. "/api/payment/anxin/notify/**", // 安心付通知(公开,供支付宝调用)
  48. "/api/payment/agreement/notify/**", // 签约回调通知(公开,供微信调用)
  49. "/api/payment/alipay/**", // 支付宝开放能力回调(公开)
  50. "/api/payment/plans", // 支付方案列表(公开)
  51. "/api/membership/levels", // 会员等级列表(公开)
  52. "/api/membership/levels/*", // 会员等级详情(公开)
  53. "/error", // 错误页面
  54. "/actuator/health" // 健康检查
  55. };
  56. @Bean
  57. public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
  58. // dev 环境启用后端 CORS 兜底(DevCorsConfig 提供配置源),生产环境由 Nginx 统一处理
  59. boolean isDev = environment.acceptsProfiles(org.springframework.core.env.Profiles.of("dev"));
  60. if (isDev) {
  61. http.cors(cors -> {}); // 使用默认,自动发现 CorsConfigurationSource Bean
  62. } else {
  63. http.cors(AbstractHttpConfigurer::disable);
  64. }
  65. http
  66. .csrf(AbstractHttpConfigurer::disable)
  67. .sessionManagement(session ->
  68. session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
  69. .authorizeHttpRequests(auth -> auth
  70. // OPTIONS 预检请求直接放行(CORS 由 Nginx 处理,此处仅作兜底避免 401)
  71. .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
  72. // 超级管理员专属:角色赋权
  73. .requestMatchers("/api/admin/users/**").hasRole("SUPER_ADMIN")
  74. // 运营后台接口
  75. .requestMatchers("/api/admin/**").hasAnyRole("ADMIN", "SUPER_ADMIN")
  76. // 散落在用户路径中的管理员操作(已统一到 /api/admin/** 下)
  77. .requestMatchers("/api/coupon/issue").hasAnyRole("ADMIN", "SUPER_ADMIN")
  78. // 用户端邀请接口需登录(必须在 /api/invite/{code} 公开匹配之前)
  79. .requestMatchers("/api/invite/code", "/api/invite/stats",
  80. "/api/invite/rewards", "/api/invite/extra-quota",
  81. "/api/invite/bind").authenticated()
  82. .requestMatchers(PUBLIC_URLS).permitAll()
  83. .anyRequest().authenticated()
  84. )
  85. .exceptionHandling(ex -> ex
  86. .authenticationEntryPoint((request, response, authException) -> {
  87. response.setStatus(HttpStatus.UNAUTHORIZED.value());
  88. response.setContentType(MediaType.APPLICATION_JSON_VALUE);
  89. response.setCharacterEncoding(StandardCharsets.UTF_8.name());
  90. Result<Void> result = Result.error(ErrorCode.UNAUTHORIZED);
  91. response.getWriter().write(new ObjectMapper().writeValueAsString(result));
  92. })
  93. .accessDeniedHandler((request, response, accessDeniedException) -> {
  94. response.setStatus(HttpStatus.FORBIDDEN.value());
  95. response.setContentType(MediaType.APPLICATION_JSON_VALUE);
  96. response.setCharacterEncoding(StandardCharsets.UTF_8.name());
  97. Result<Void> result = Result.error(ErrorCode.FORBIDDEN);
  98. response.getWriter().write(new ObjectMapper().writeValueAsString(result));
  99. })
  100. )
  101. .addFilterBefore(jwtAuthenticationFilter,
  102. UsernamePasswordAuthenticationFilter.class);
  103. return http.build();
  104. }
  105. @Bean
  106. public PasswordEncoder passwordEncoder() {
  107. return new BCryptPasswordEncoder();
  108. }
  109. }