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

67 lines
4.1 KiB
Markdown

---
title: error / method-security CGLIB vs JDK proxy — use case injection + unauth exception type
source_type: error-note
status: raw
related_branches: [feature-authentication-authorization-contract]
related_projects: [ca-skeleton]
tags: [error, ca-skeleton, spring-security, method-security, aop, proxy, cglib, authorization]
created: 2026-06-08
status_label: 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]] — `@RequiresPermission` enforcement(`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 강제:
```java
@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
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).