Browse Source

fix(security): CORS 拆分为 SDK 开放与管理白名单双轨

修复 9338fcb 收紧 CORS 白名单后引发的线上 403 回归:
将 corsConfigurationSource 与 addCorsMappings 拆分为两套配置——SDK 第三方接入接口
(/ai/**、/category/tree、/category/list、/feedback、/attachment/upload)开放 allowedOriginPatterns("*"),
管理后台接口保持 allowed-origins 白名单防 CSRF;并在 application-prod.yml 白名单补上线上域名
https://aidoc.dgscdw.com:11300,同时记录 CORS 双轨制踩坑规范。
Spring-AI-1.1.2
wanghanlin 3 weeks ago
parent
commit
3038cd0387
  1. 15
      CLAUDE.md
  2. 14
      src/main/java/com/wok/supportbot/config/CorsConfig.java
  3. 30
      src/main/java/com/wok/supportbot/security/SecurityConfig.java
  4. 2
      src/main/resources/application-prod.yml

15
CLAUDE.md

@ -201,6 +201,21 @@ catch (e) { toast('操作失败', 'error') }
**反面案例**: commit `527d9e7` 创建的流程图展示了 RAG 子图中 `VECTOR → RRF | KEYWORD → RRF | HYBRID → RRF → Reranker` 的三模式检索流程,但实际 `RagPipeline.similaritySearch()` 仅做纯向量检索,`HybridSearchService`/`RrfFusion`/`RerankerService` 尚未接入主对话流程,导致图表与代码事实不符。
### 后端:CORS 双轨制(SDK 接口开放 / 管理接口白名单)
**规则**: CORS 配置分两套,**不可整体收紧为单一白名单**(系统可用性第一):
| 路径 | CORS 策略 | 原因 |
|------|----------|------|
| SDK 第三方接入接口:`/ai/**`、`/category/tree`、`/category/list`、`/feedback`、`/attachment/upload` | `allowedOriginPatterns("*")` 开放跨域 | 第三方域名动态未知,用 Bearer Token 鉴权,不依赖来源白名单 |
| 管理后台接口:其余 `/**` | `allowed-origins` 白名单 | 用 httpOnly refresh cookie,需白名单防跨域 CSRF |
配置有两处,**必须同步修改**:`SecurityConfig.corsConfigurationSource()`(Security 链 CorsFilter)与 `CorsConfig.addCorsMappings()`(Spring MVC 层 CorsInterceptor)。两者均按注册顺序匹配,**先注册精确的 SDK 路径,再注册兜底的 `/**`**。
**反面案例**: commit `9338fcb` 为配合 refresh token 迁移 httpOnly Cookie,把 CORS 从 `allowedOriginPatterns("*")` 整体收紧为白名单,引发两个回归:(1) 同源静态资源 `<script crossorigin>`/`<link crossorigin>` 带 `Origin` 头、白名单缺线上域名 → 首页空白 + assets 403;(2) SDK 第三方跨域、第三方域名不在白名单 → 接口 403。教训:改 CORS 必须同时考虑「同源静态资源的 crossorigin 属性」和「SDK 第三方跨域」两个场景。
**部署提醒**: 更换前端域名/端口时,必须同步把新域名加入 `application-prod.yml``app.cors.allowed-origins`,否则同源静态资源会 403。
## API 路由约定
- AI 对话: `/ai/*`(`AiController`)

14
src/main/java/com/wok/supportbot/config/CorsConfig.java

@ -27,11 +27,19 @@ public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
// 覆盖所有请求
// SDK 第三方接入接口开放跨域第三方域名动态未知使用 Bearer Token 鉴权 refresh cookie 暴露风险
for (String path : new String[]{"/ai/**", "/category/tree", "/category/list", "/feedback", "/attachment/upload"}) {
registry.addMapping(path)
.allowCredentials(true)
.allowedOriginPatterns("*")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.exposedHeaders("*");
}
// 管理后台接口来源白名单保护 httpOnly refresh cookie防跨域 CSRF
registry.addMapping("/**")
// 允许发送 Cookierefresh token 需要
.allowCredentials(true)
// 放行白名单域名禁止 * + allowCredentials 组合
.allowedOriginPatterns(allowedOrigins.toArray(new String[0]))
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")

30
src/main/java/com/wok/supportbot/security/SecurityConfig.java

@ -128,14 +128,30 @@ public class SecurityConfig {
*/
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOriginPatterns(allowedOrigins);
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
config.setAllowedHeaders(List.of("*"));
config.setExposedHeaders(List.of("*"));
config.setAllowCredentials(true);
// SDK 第三方接入接口开放跨域第三方域名动态未知使用 Bearer Token 鉴权 refresh cookie 暴露风险
CorsConfiguration sdkConfig = new CorsConfiguration();
sdkConfig.setAllowedOriginPatterns(List.of("*"));
sdkConfig.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
sdkConfig.setAllowedHeaders(List.of("*"));
sdkConfig.setExposedHeaders(List.of("*"));
sdkConfig.setAllowCredentials(true);
// 管理后台接口来源白名单保护 httpOnly refresh cookie防跨域 CSRF
CorsConfiguration adminConfig = new CorsConfiguration();
adminConfig.setAllowedOriginPatterns(allowedOrigins);
adminConfig.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
adminConfig.setAllowedHeaders(List.of("*"));
adminConfig.setExposedHeaders(List.of("*"));
adminConfig.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
// 先注册精确的 SDK 路径再注册兜底的 /**保证 SDK 接口优先命中开放配置
source.registerCorsConfiguration("/ai/**", sdkConfig);
source.registerCorsConfiguration("/category/tree", sdkConfig);
source.registerCorsConfiguration("/category/list", sdkConfig);
source.registerCorsConfiguration("/feedback", sdkConfig);
source.registerCorsConfiguration("/attachment/upload", sdkConfig);
source.registerCorsConfiguration("/**", adminConfig);
return source;
}
}

2
src/main/resources/application-prod.yml

@ -36,7 +36,7 @@ jwt:
# ==================== CORS 白名单(生产替换为真实域名) ====================
app:
cors:
allowed-origins: https://docfilepath.dgscdw.com:8804
allowed-origins: https://docfilepath.dgscdw.com:8804,https://aidoc.dgscdw.com:11300
# ==================== 文件存储路径 ====================
knowledge:

Loading…
Cancel
Save