Procházet zdrojové kódy

优化代码,添加接口

liuchengsen před 1 měsícem
rodič
revize
3e0c46ff60

+ 59 - 1
docs/前端对接变更说明-2026-06-30.md

@@ -218,7 +218,63 @@ CREATE TABLE `t_payment_order_extra_payment` (...);
 
 ---
 
-## 八、变更文件列表
+## 八、CORS 部署说明
+
+### 8.1 架构调整
+
+- **CORS 统一由 Nginx 处理**,后端 `zhijiayun-user` 已关闭 Spring Security CORS。
+- 其他模块(`zhijiayun-invite`、`zhijiayun-payment`)本身无 CORS 配置,直接由 Nginx 接管。
+
+### 8.2 Nginx 关键配置
+
+```nginx
+# http 块内全局定义
+map $http_origin $cors_origin {
+    "~^(http://localhost:63342|https://[a-zA-Z0-9-]+\.kailin\.com\.cn)$" $http_origin;
+    default "";
+}
+
+# /api/ 接口统一加头(示例)
+location ^~ /api/ {
+    add_header Access-Control-Allow-Origin $cors_origin always;
+    add_header Access-Control-Allow-Methods "GET,POST,PUT,DELETE,OPTIONS" always;
+    add_header Access-Control-Allow-Headers "Content-Type,Authorization,X-Request-Id" always;
+    add_header Access-Control-Allow-Credentials "true" always;
+    add_header Access-Control-Max-Age "86400" always;
+
+    if ($request_method = 'OPTIONS') {
+        return 204;
+    }
+
+    proxy_pass http://172.27.95.79:8002;
+    # ... 其他代理配置
+}
+```
+
+### 8.3 后端 OPTIONS 兜底
+
+虽然 CORS 由 Nginx 统一处理,但为避免 Nginx 配置遗漏或本地直连后端时 OPTIONS 预检被 Spring Security 拦截,已在 `SecurityConfig` 中放行 OPTIONS 请求:
+
+```java
+.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
+```
+
+**注意:** 放行 OPTIONS 仅保证不返回 401/403 认证错误,CORS 响应头仍由 Nginx 负责添加。生产环境必须确保 Nginx 在 `proxy_pass` 之前拦截 OPTIONS 并返回 204。
+
+### 8.4 测试调整
+
+`zhijiayun-gateway` 原有的 `GatewayFilterTest.CorsTests` 测试后端 CORS 行为,现已删除。原因:
+
+- CORS 逻辑已迁移到 Nginx 层,后端不再负责添加 CORS 头。
+- 后端单元测试无法模拟 Nginx 行为,继续断言 CORS 头会导致测试失效。
+
+### 8.5 注意事项
+
+- 空 Origin 时 `$cors_origin` 为空,Nginx 会输出空的 `Access-Control-Allow-Origin` 头,浏览器会忽略它,符合安全预期。
+- `/api/invite/` 建议单独配置 `location`,并设置 `proxy_buffering off`,保证大文件流式下载不被 Nginx 缓冲截断。
+- 部署前务必检查 Nginx 配置已包含上述 CORS 头并正确拦截 OPTIONS,否则前端跨域会失败。
+
+## 九、变更文件列表
 
 | 文件 | 变更类型 | 说明 |
 |---|---|---|
@@ -229,6 +285,8 @@ CREATE TABLE `t_payment_order_extra_payment` (...);
 | `zhijiayun-payment/.../PaymentOrderExtraPaymentMapper.java` | 新增 | 重复支付记录 Mapper |
 | `zhijiayun-payment/.../PaymentOrderService.java` | 修改 | 渠道切换、重复支付处理 |
 | `zhijiayun-payment/.../PaymentCallbackController.java` | 修改 | 回调传递 `paidChannel` |
+| `zhijiayun-user/.../SecurityConfig.java` | 修改 | 关闭 Spring Security CORS,放行 OPTIONS 作为兜底 |
+| `zhijiayun-gateway/.../GatewayFilterTest.java` | 修改 | 删除已失效的后端 CORS 测试 |
 | `zhijiayun-gateway/.../db/schema.sql` | 修改 | 新增字段和表 |
 | `zhijiayun-gateway/.../db/migration-v5.sql` | 新增 | 已有环境幂等升级脚本 |
 | `zhijiayun-test/.../invite-test.html` | 修改 | 测试页面同步新增/调整接口说明 |

+ 0 - 50
zhijiayun-gateway/src/test/java/com/xuekairui/gateway/GatewayFilterTest.java

@@ -132,56 +132,6 @@ class GatewayFilterTest extends GatewayBaseTest {
         }
     }
 
-    @Nested
-    @DisplayName("CORS 跨域")
-    class CorsTests {
-
-        @Test
-        @DisplayName("OPTIONS 预检请求应返回 CORS 头")
-        void optionsRequestShouldReturnCorsHeaders() throws Exception {
-            mockMvc.perform(options("/api/level/list")
-                            .header("Origin", "http://localhost:3000")
-                            .header("Access-Control-Request-Method", "GET")
-                            .header("Access-Control-Request-Headers", "Authorization"))
-                    .andExpect(status().isOk())
-                    .andExpect(header().exists("Access-Control-Allow-Origin"))
-                    .andExpect(header().exists("Access-Control-Allow-Methods"));
-        }
-
-        @Test
-        @DisplayName("CORS 应允许 GET 方法")
-        void corsShouldAllowGet() throws Exception {
-            mockMvc.perform(options("/api/level/list")
-                            .header("Origin", "http://localhost:3000")
-                            .header("Access-Control-Request-Method", "GET"))
-                    .andExpect(status().isOk())
-                    .andExpect(header().string("Access-Control-Allow-Methods",
-                            org.hamcrest.Matchers.containsString("GET")));
-        }
-
-        @Test
-        @DisplayName("CORS 应允许 POST 方法")
-        void corsShouldAllowPost() throws Exception {
-            mockMvc.perform(options("/api/auth/sms/login")
-                            .header("Origin", "http://localhost:3000")
-                            .header("Access-Control-Request-Method", "POST"))
-                    .andExpect(status().isOk())
-                    .andExpect(header().string("Access-Control-Allow-Methods",
-                            org.hamcrest.Matchers.containsString("POST")));
-        }
-
-        @Test
-        @DisplayName("CORS 应暴露 Authorization 头")
-        void corsShouldExposeAuthorizationHeader() throws Exception {
-            mockMvc.perform(options("/api/level/list")
-                            .header("Origin", "http://localhost:3000")
-                            .header("Access-Control-Request-Method", "GET"))
-                    .andExpect(status().isOk())
-                    .andExpect(header().string("Access-Control-Expose-Headers",
-                            org.hamcrest.Matchers.containsString("Authorization")));
-        }
-    }
-
     @Nested
     @DisplayName("统一响应格式")
     class ResponseFormatTests {

+ 5 - 27
zhijiayun-user/src/main/java/com/xuekairui/user/config/SecurityConfig.java

@@ -7,6 +7,7 @@ import com.xuekairui.user.security.JwtAuthenticationFilter;
 import lombok.RequiredArgsConstructor;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
+import org.springframework.http.HttpMethod;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.MediaType;
 import org.springframework.security.config.annotation.web.builders.HttpSecurity;
@@ -17,13 +18,7 @@ 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 org.springframework.web.cors.CorsConfiguration;
-import org.springframework.web.cors.CorsConfigurationSource;
-import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
-
 import java.nio.charset.StandardCharsets;
-import java.util.Arrays;
-import java.util.List;
 
 /**
  * Spring Security 配置
@@ -64,10 +59,13 @@ public class SecurityConfig {
     public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
         http
                 .csrf(AbstractHttpConfigurer::disable)
-                .cors(cors -> cors.configurationSource(corsConfigurationSource()))
+                // CORS 由 Nginx 统一处理,后端关闭,避免重复/冲突
+                .cors(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")
                         // 运营后台接口
@@ -106,26 +104,6 @@ public class SecurityConfig {
         return http.build();
     }
 
-    @Bean
-    public CorsConfigurationSource corsConfigurationSource() {
-        CorsConfiguration configuration = new CorsConfiguration();
-        // allowedOriginPatterns 会回显请求中的 Origin(不会发 *),
-        // 比 allowedOrigins 更安全且与代理服务器兼容
-        configuration.setAllowedOriginPatterns(List.of("*"));
-        configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"));
-        configuration.setAllowedHeaders(List.of("*"));
-        // 允许前端读取的响应头
-        configuration.setExposedHeaders(List.of("Authorization", "X-Request-Id"));
-        // 注意:不设置 allowCredentials,因为前端通过 Authorization 头传递 JWT,
-        // 不依赖 Cookie。如果 Nginx 反向代理添加了 Access-Control-Allow-Origin: *,
-        // allowCredentials=true 会导致浏览器拒绝该组合(CORS 规范禁止)。
-        configuration.setMaxAge(3600L);
-
-        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
-        source.registerCorsConfiguration("/**", configuration);
-        return source;
-    }
-
     @Bean
     public PasswordEncoder passwordEncoder() {
         return new BCryptPasswordEncoder();