SecurityConfig.java 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
  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/admin/auth/login", // 管理员登录(公开)
  36. "/api/auth/password/login", // 密码登录
  37. "/api/auth/refresh", // 刷新Token(无需认证)
  38. "/api/auth/wechat/**", // 微信登录相关
  39. "/api/invite/validate", // 验证邀请码(注册前)
  40. "/api/invite/{code}", // 邀请链接入口(公开)
  41. "/api/invite/page/**", // 邀请落地页(公开)
  42. "/api/invite/click/**", // 邀请链接点击追踪(公开)
  43. "/api/invite/resolve-link", // 解析邀请链接/邀请码(公开)
  44. "/invite/**", // 浏览器直接访问邀请链接(公开)
  45. "/api/platform-config/**", // 平台配置查询(公开)
  46. "/api/search/suggestions", // 搜索建议(公开)
  47. "/api/payment/callback/**", // 支付回调(公开,供微信/支付宝调用)
  48. "/api/payment/anxin/notify/**", // 安心付通知(公开,供支付宝调用)
  49. "/api/payment/agreement/notify/**", // 签约回调通知(公开,供微信调用)
  50. "/api/payment/alipay/**", // 支付宝开放能力回调(公开)
  51. "/api/payment/plans", // 支付方案列表(公开)
  52. "/api/membership/levels", // 会员等级列表(公开)
  53. "/api/membership/levels/*", // 会员等级详情(公开)
  54. "/error", // 错误页面
  55. "/actuator/health" // 健康检查
  56. };
  57. @Bean
  58. public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
  59. // dev 环境启用后端 CORS 兜底(DevCorsConfig 提供配置源),生产环境由 Nginx 统一处理
  60. boolean isDev = environment.acceptsProfiles(org.springframework.core.env.Profiles.of("dev"));
  61. if (isDev) {
  62. http.cors(cors -> {}); // 使用默认,自动发现 CorsConfigurationSource Bean
  63. } else {
  64. http.cors(AbstractHttpConfigurer::disable);
  65. }
  66. http
  67. .csrf(AbstractHttpConfigurer::disable)
  68. .sessionManagement(session ->
  69. session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
  70. .authorizeHttpRequests(auth -> auth
  71. // OPTIONS 预检请求直接放行(CORS 由 Nginx 处理,此处仅作兜底避免 401)
  72. .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
  73. // 管理员登录必须在 /api/admin/** 鉴权之前,否则会被拦截返回 401
  74. .requestMatchers("/api/admin/auth/login").permitAll()
  75. // 超级管理员专属:角色赋权
  76. .requestMatchers("/api/admin/users/**").hasRole("SUPER_ADMIN")
  77. // 运营后台接口
  78. .requestMatchers("/api/admin/**").hasAnyRole("ADMIN", "SUPER_ADMIN")
  79. // 散落在用户路径中的管理员操作(已统一到 /api/admin/** 下)
  80. .requestMatchers("/api/coupon/issue").hasAnyRole("ADMIN", "SUPER_ADMIN")
  81. // 用户端邀请接口需登录(必须在 /api/invite/{code} 公开匹配之前)
  82. .requestMatchers("/api/invite/code", "/api/invite/stats",
  83. "/api/invite/rewards", "/api/invite/extra-quota",
  84. "/api/invite/bind").authenticated()
  85. .requestMatchers(PUBLIC_URLS).permitAll()
  86. .anyRequest().authenticated()
  87. )
  88. .exceptionHandling(ex -> ex
  89. .authenticationEntryPoint((request, response, authException) -> {
  90. response.setStatus(HttpStatus.UNAUTHORIZED.value());
  91. response.setContentType(MediaType.APPLICATION_JSON_VALUE);
  92. response.setCharacterEncoding(StandardCharsets.UTF_8.name());
  93. Result<Void> result = Result.error(ErrorCode.UNAUTHORIZED);
  94. response.getWriter().write(new ObjectMapper().writeValueAsString(result));
  95. })
  96. .accessDeniedHandler((request, response, accessDeniedException) -> {
  97. response.setStatus(HttpStatus.FORBIDDEN.value());
  98. response.setContentType(MediaType.APPLICATION_JSON_VALUE);
  99. response.setCharacterEncoding(StandardCharsets.UTF_8.name());
  100. Result<Void> result = Result.error(ErrorCode.FORBIDDEN);
  101. response.getWriter().write(new ObjectMapper().writeValueAsString(result));
  102. })
  103. )
  104. .addFilterBefore(jwtAuthenticationFilter,
  105. UsernamePasswordAuthenticationFilter.class);
  106. return http.build();
  107. }
  108. @Bean
  109. public PasswordEncoder passwordEncoder() {
  110. return new BCryptPasswordEncoder();
  111. }
  112. }