SecurityConfig.java 6.2 KB

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