4.1 KiB
title, source_type, status, related_branches, related_projects, tags, created, status_label
| title | source_type | status | related_branches | related_projects | tags | created | status_label | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| error / method-security CGLIB vs JDK proxy — use case injection + unauth exception type | error-note | raw |
|
|
|
2026-06-08 | resolved |
error: method-security AOP proxy — use case 주입 실패 + unauthenticated 예외 타입
Layer:
raw/errors/—@EnableMethodSecurity로 use case bean 을 proxy 할 때 만난 두 가지 함정.
Parent / 부모
- raw/branch-notes/feature-authentication-authorization-contract —
@RequiresPermissionenforcement(RequiresPermissionAuthorizationManager+MethodSecurityConfig) 구현 중 발생.
증상 1 / Symptom — BeanNotOfRequiredTypeException
WorkLogAuthorizationContractTest(@SpringBootTest, classes=nested @Configuration) 가 4 케이스 전부
org.springframework.beans.factory.UnsatisfiedDependencyException
Caused by: org.springframework.beans.factory.BeanNotOfRequiredTypeException
로 실패. @Autowired CreateWorkLogUseCase 가 만족 안 됨.
근본 원인
method-security 의 custom Advisor 가 @RequiresPermission use case 를 AOP proxy 로 감쌌는데, JDK dynamic proxy 가 생성됨. JDK proxy 는 use case 가 구현한 인터페이스(CommandUseCase)만 구현하고 concrete CreateWorkLogUseCase 의 subtype 이 아니다. controller(WorkLogController)와 test 는 concrete *UseCase 타입을 주입받으므로 assign 불가.
prod 앱(CaSkeletonApplication, @SpringBootApplication)은 Spring Boot 의 AopAutoConfiguration 이 spring.aop.proxy-target-class=true(CGLIB class proxy) 를 기본 적용 → concrete subtype proxy → 주입 정상. 그러나 auto-config 가 없는 isolated test slice 에는 그 기본이 안 들어와 JDK proxy 로 fallback.
해소
contract test 의 nested config 에 CGLIB 강제:
@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = true)
@Import(MethodSecurityConfig.class)
static class AuthzTestConfig { ... }
이는 prod 의 AOP 기본을 mirror 하는 것이라 prod 동작 변경 없음. 교훈: method security 를 거는 bean 을 concrete 타입으로 주입한다면 반드시 CGLIB proxy 여야 한다. Boot 앱은 자동이지만, slice/standalone context 는 명시 필요.
증상 2 / Symptom — unauthenticated 가 403 아님
unauthenticated_caller_is_denied_fail_closed 테스트가 AccessDeniedException 을 기대했으나 실제론 AuthenticationCredentialsNotFoundException 발생 → 단언 실패.
근본 원인
AuthorizationManagerBeforeMethodInterceptor 는 Supplier<Authentication> 을 deferred 로 넘기는데, SecurityContext 가 비어 있으면(getAuthentication()==null) .get() 호출 시 AuthenticationCredentialsNotFoundException(= AuthenticationException, 401-family) 을 던진다. 즉 권한 부족(403) 과 인증 자체 없음(401) 은 다른 경로다. 내 RequiresPermissionAuthorizationManager.check 의 auth==null 분기는 supplier 가 먼저 throw 하므로 unauthenticated 케이스에선 도달하지 않는다(authenticated-but-not-authorized 토큰 케이스에서만 도달).
해소
테스트 단언을 isInstanceOf(AuthenticationException.class) 로 정정. prod 에서는 security filter chain(.anyRequest().authenticated())이 method-security 도달 전에 401(EnvelopeAuthenticationEntryPoint)로 차단하므로, method-security 의 unauth 경로는 defense-in-depth backstop 으로만 의미.
교훈 / Lesson
- method-secured bean 을 concrete 타입으로 DI 하면 CGLIB(
proxyTargetClass=true) 필수. Boot 앱은 자동, slice 는 수동. - method-security 단의 거부는 두 종류: 인증 없음 →
AuthenticationException(401), 권한 부족 →AccessDeniedException(403). 테스트·핸들러 매핑을 분리해 생각해야 함. - AOP self-invocation/non-bean 호출은 proxy 우회 → mutating 진입점이 전부 Spring bean 경유인지 정적 검증 필요(ArchUnit, host=architecture-enforcement-rules).