[Feat/#24] 인증 사용자 주입을 위한 ApiUser와 ArgumentResolver 추가 - #25
Merged
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
#️⃣연관된 이슈
🎯 해결하려는 문제가 무엇인가요?
컨트롤러가 인증된 사용자를 파라미터로 주입받는 통로가 없습니다.
인증 체인 자체는 이미 동작합니다 —
JwtAuthFilter가 토큰을 검증해UserAuthentication을SecurityContextHolder에 넣고,SecurityConfig가/v1/app/**→STUDENT,/v1/admin/**→ADMIN으로 1차 인가하며,PrincipalProvider/SecurityPrincipalProvider가userId를 꺼내줍니다. 비어 있는 것은 컨트롤러 메서드가 그 값을 받는 마지막 한 칸뿐입니다.config-and-auth.md5절이 규정한ApiUser·구현체·ArgumentResolver·등록 설정이 저장소에 하나도 없었습니다.❓ 왜 해결해야 하나요?
사용자 식별이 필요한 모든 API가 이것 없이는 시작할 수 없습니다. 당장 #23 행사 신청 API가 막혀 있고,
app-api·admin-api를 쓰는 다른 작업들도 같은 지점에서 멈춥니다. 열려 있는 PR 어디에도 이 인프라가 없어 별도 이슈로 분리했습니다.⭐ 어떻게 해결했나요?
WebMvcConfig를common-api에 두되common-api가app-api/admin-api를 컴파일 타임에 참조하지 않습니다. 리졸버를 타입으로 지목하지 않고 주입받아 일괄 등록합니다.bootstrap이 세 모듈을 모두 조립하므로 런타임에 스프링이 리졸버 빈을 모아 넘깁니다. 클라이언트 모듈이 추가돼도WebMvcConfig는 수정할 필요가 없습니다.리졸버 동작은
config-and-auth.md4-3절 그대로입니다 — 파라미터 타입이 맞으면 해당 role을 확인하고, 없으면BusinessException(FORBIDDEN), 있으면 스냅샷을 주입합니다.새로 추가한 Gradle 의존이 없습니다. 세 모듈 모두 이미
core:common·gateway:auth·spring-boot-starter-webmvc를 갖고 있습니다.🧩 이 PR의 한계 & 트레이드오프
ApiUser가userId()만 노출합니다. 리졸버가 이미 role을 검증하고 통과시키므로 컨트롤러가 role을 다시 볼 일이 없다고 판단했습니다. 필요해지면 그때 넓히는 편이 안전합니다.AdminApiUser에 부서(CouncilDepartment)를 담지 않았습니다.config-and-auth.md4-4절이 부서 단위 인가를DepartmentAccessChecker(별도 클래스) 소관으로 "이 프로젝트의 기본"이라 규정합니다. 그 클래스는 아직 없어 별도 이슈가 필요합니다.⛓️ 기존 기능에 미치는 영향
WebMvcConfig가 처음 생기는WebMvcConfigurer입니다. 등록하는 리졸버는 스프링 기본 리졸버보다 앞에 삽입되지만,ApiUser타입 파라미터만 가져가므로 기존 파라미터 바인딩에 영향이 없습니다.api/app-api/src,api/admin-api/src가 이 PR로 처음 생깁니다. [Feat/#19] 게시판(공지) 목록/상세 조회 API 추가 #21(공지 API)·행사 신청서 폼 조회/신청 API 추가 #23(행사 API)이 같은 디렉터리에 컨트롤러를 추가하지만 파일이 겹치지 않습니다.gradle check(ModularityTests.verify()+DomainImplAccessTests) 통과를 clean 빌드에서 확인했습니다.🔀 Edge Case & 실패 시나리오
BusinessException(CommonErrorCode.FORBIDDEN)→ 403.SecurityConfig의 URL 패턴 인가가 1차, 이것이 2차 방어선SecurityPrincipalProvider가UNAUTHORIZED→ 401ObjectProvider.orderedStream()이 빈 스트림이라 기동은 정상. 파라미터는 아래 항목대로 처리됨ApiUser파라미터가 선언됨📋 검토한 대안과 선택 이유
1. 컨트롤러에
PrincipalProvider를 직접 주입 — 이미 있는 것만으로 끝나지만,config-and-auth.md가 규정한ApiUser패턴과 다른 선례가 남고 컨트롤러 시그니처에 인증 요구가 드러나지 않습니다. 채택하지 않았습니다.2.
List<HandlerMethodArgumentResolver>를 그대로 주입 — 컨텍스트에 등록된 무관한 리졸버까지 함께 수집될 수 있어, 마커 인터페이스ApiUserArgumentResolver로 좁혔습니다.3.
List<ApiUserArgumentResolver>생성자 주입 — 스프링은 후보 빈이 하나도 없으면 주입에 실패하므로 모듈 단위 슬라이스 테스트에서 기동이 깨집니다.ObjectProvider로 받아addArgumentResolvers()시점에 펼칩니다.4.
AppApiUser/AdminApiUser를record로 선언 — 처음에는 record였으나 아래 이유로 생성자를 감춘 클래스로 바꿨습니다.💬 리뷰 포인트
[r]AppApiUser/AdminApiUser가 record가 아닌 이유ApiUser구현체를 어노테이션 없는 record 파라미터로 두면, 리졸버가 등록되지 않은 컨텍스트에서 인증이 열린 채로 실패합니다.RequestMappingHandlerAdapter.getDefaultArgumentResolvers()는 커스텀 리졸버 뒤에 폴백으로ServletModelAttributeMethodProcessor(annotationNotRequired = true)를 등록합니다. 이 폴백은BeanUtils.isSimpleProperty()가 false인 모든 타입을 가져가고, Spring 6부터 record를 요청 파라미터로 생성자 바인딩합니다. 결과적으로GET /v1/app/foo?userId=999가new AppApiUser(999L)을 만들어 컨트롤러가 공격자가 넣은 userId로 동작하게 됩니다. Spring 7.0.8 바이트코드에서 확인했습니다.현재 조립된 앱에서는 재현되지 않습니다(
bootstrap이 리졸버를 항상 스캔). 다만@WebMvcTest슬라이스는WebMvcConfigurer를 포함하면서 일반@Component는 제외하므로 리졸버 리스트가 비고, 이때 폴백이 먹습니다. 앞으로ApiUser타입을 추가하면서 리졸버를 빠뜨려도 같습니다.그래서 두 타입을 private 생성자 + 정적 팩토리 클래스로 두었습니다. 폴백의
BeanUtils.getResolvableConstructor()가 public 생성자도 no-arg 생성자도 찾지 못해 예외를 던지므로, 조용한 스푸핑 대신 500으로 닫힙니다.coding-style.md2-10절의 기본 규칙(정적 팩토리 + 생성자 감추기)에도 부합하고gateway:auth의UserAuthentication과 같은 형태입니다.[c]네이밍이 컨벤션 문서와 다릅니다config-and-auth.md4-5절은{Role}ApiUser(즉StudentApiUser)로 적혀 있으나, 컨트롤러가App*Controller/Admin*Controller이고 모듈도app-api/admin-api라 클라이언트 기준 네이밍으로 갔습니다. 패키지도{client}/{Client}ApiUser+{client}/resolver/...형태로 맞췄습니다.작업 중 같은 문서의 다른 드리프트도 확인했습니다 — 문서의
Department·DEPT_접두사가 실제 코드에서는CouncilDepartment·COUNCIL_입니다. 문서 정리는 별도 이슈로 다루는 게 좋겠습니다.[a]ApiUser인터페이스를 직접 파라미터로 선언하는 경우두 리졸버가 구체 타입
equals로만 매칭하므로ApiUser자체를 선언하면 폴백이 인터페이스를 생성하려다 실패해 500이 납니다. 닫히는 방향이라 위험하진 않지만, 공개한 추상 타입이라 명시적으로 막을지 의견 주시면 반영하겠습니다.