From 22d073f5637c5482fa34c7684c098d227edc2c95 Mon Sep 17 00:00:00 2001 From: johngreen Date: Wed, 29 Apr 2026 08:35:08 +0900 Subject: [PATCH] =?UTF-8?q?SecurityConfig:=20Spring=20Security=206=20?= =?UTF-8?q?=ED=98=B8=ED=99=98=20=ED=95=84=ED=84=B0=20=EB=93=B1=EB=A1=9D=20?= =?UTF-8?q?=EC=88=9C=EC=84=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 기존 코드는 addFilterBefore(AiApi, JwtAuthenticationFilter.class) 가 jwt 등록 전에 호출되어 'JwtAuthenticationFilter does not have a registered order' 예외 발생. Spring Security 6 부터 anchor 는 _이미 등록된_ 필터여야 함. 수정: jwt 를 UsernamePasswordAuthenticationFilter 기준으로 가장 먼저 등록 → JwtAuthenticationFilter.class 가 known map 에 추가됨 → 이후 SubdomainResolver/AiApiKey/TenantConsistency/ForcePw 가 jwt 를 anchor 로 정상 참조. 실행 순서는 동일: SubdomainResolver → AiApiKey → Jwt → TenantConsistency → ForcePw → UsernamePasswordAuthenticationFilter Co-Authored-By: Claude Opus 4.7 (1M context) --- .../java/com/erp/security/SecurityConfig.java | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/backend-spring/src/main/java/com/erp/security/SecurityConfig.java b/backend-spring/src/main/java/com/erp/security/SecurityConfig.java index ebad3e01..7a88baee 100644 --- a/backend-spring/src/main/java/com/erp/security/SecurityConfig.java +++ b/backend-spring/src/main/java/com/erp/security/SecurityConfig.java @@ -59,21 +59,26 @@ public class SecurityConfig { .requestMatchers("/api/**").permitAll() .anyRequest().authenticated() ) - .addFilterBefore(new AiApiKeyAuthFilter(aiAgentApiKeyService), - JwtAuthenticationFilter.class) + // ⚠️ Spring Security 6 부터 addFilterBefore/After 의 anchor 는 _이미 등록된_ + // 필터여야 함. 따라서 JwtAuthenticationFilter 를 가장 먼저 (Spring 표준 + // UsernamePasswordAuthenticationFilter 기준으로) 등록한 뒤, 나머지 커스텀 + // 필터들이 JwtAuthenticationFilter 를 anchor 로 사용한다. .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class) + // Phase 2 (2026-04-24): 서브도메인 → CompanyResolver → TenantRoutingDataSource 라우팅. + // JwtAuthenticationFilter 보다 앞에서 실행되어야 tenant 컨텍스트가 먼저 결정됨. + .addFilterBefore( + new SubdomainResolverFilter(companyResolver, tenantRoutingDataSource, tenantDbSettings), + JwtAuthenticationFilter.class) + // AiApi 키 인증 — jwt 앞에서 sk-pipe-* 형식 처리, 매칭되지 않으면 jwt 로 통과. + .addFilterBefore(new AiApiKeyAuthFilter(aiAgentApiKeyService), + JwtAuthenticationFilter.class) // JwtAuthenticationFilter 뒤 — JWT.company_code 와 서브도메인의 company_code 대조. .addFilterAfter(new TenantConsistencyGuardFilter(jwtTokenProvider), JwtAuthenticationFilter.class) // TenantConsistencyGuardFilter 뒤 — 비번 강제 변경 대기자는 허용 경로만 통과. .addFilterAfter(new ForcePasswordChangeGuardFilter(jwtTokenProvider), - TenantConsistencyGuardFilter.class) - // Phase 2 (2026-04-24): 서브도메인 → CompanyResolver → TenantRoutingDataSource 라우팅. - // JwtAuthenticationFilter 보다 앞에서 실행되어야 tenant 컨텍스트가 먼저 결정됨. - .addFilterBefore( - new SubdomainResolverFilter(companyResolver, tenantRoutingDataSource, tenantDbSettings), - JwtAuthenticationFilter.class); + TenantConsistencyGuardFilter.class); return http.build(); }