Files
llm-wiki/raw/errors/method-security-cglib-vs-jdk-proxy-usecase-injection-2026-06-08.md

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
feature-authentication-authorization-contract
ca-skeleton
error
ca-skeleton
spring-security
method-security
aop
proxy
cglib
authorization
2026-06-08 resolved

error: method-security AOP proxy — use case 주입 실패 + unauthenticated 예외 타입

Layer: raw/errors/@EnableMethodSecurity 로 use case bean 을 proxy 할 때 만난 두 가지 함정.

Parent / 부모

증상 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 의 AopAutoConfigurationspring.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 발생 → 단언 실패.

근본 원인

AuthorizationManagerBeforeMethodInterceptorSupplier<Authentication> 을 deferred 로 넘기는데, SecurityContext 가 비어 있으면(getAuthentication()==null) .get() 호출 시 AuthenticationCredentialsNotFoundException(= AuthenticationException, 401-family) 을 던진다. 즉 권한 부족(403)인증 자체 없음(401) 은 다른 경로다. 내 RequiresPermissionAuthorizationManager.checkauth==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

  1. method-secured bean 을 concrete 타입으로 DI 하면 CGLIB(proxyTargetClass=true) 필수. Boot 앱은 자동, slice 는 수동.
  2. method-security 단의 거부는 두 종류: 인증 없음 → AuthenticationException(401), 권한 부족 → AccessDeniedException(403). 테스트·핸들러 매핑을 분리해 생각해야 함.
  3. AOP self-invocation/non-bean 호출은 proxy 우회 → mutating 진입점이 전부 Spring bean 경유인지 정적 검증 필요(ArchUnit, host=architecture-enforcement-rules).