From eee5ea97480223f26ecedab8567446421c655db9 Mon Sep 17 00:00:00 2001 From: sangrae Date: Fri, 11 Sep 2026 00:21:05 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=EC=9D=B8=EC=A6=9D=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=EC=9E=90=20=EC=8A=A4=EB=83=85=EC=83=B7=20ApiUser?= =?UTF-8?q?=EC=99=80=20=EB=A6=AC=EC=A1=B8=EB=B2=84=20=EB=93=B1=EB=A1=9D=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/kr/ac/kookmin/stream/ApiUser.java | 9 ++++++ .../stream/ApiUserArgumentResolver.java | 12 ++++++++ .../kr/ac/kookmin/stream/WebMvcConfig.java | 28 +++++++++++++++++++ 3 files changed, 49 insertions(+) create mode 100644 api/common-api/src/main/java/kr/ac/kookmin/stream/ApiUser.java create mode 100644 api/common-api/src/main/java/kr/ac/kookmin/stream/ApiUserArgumentResolver.java create mode 100644 api/common-api/src/main/java/kr/ac/kookmin/stream/WebMvcConfig.java diff --git a/api/common-api/src/main/java/kr/ac/kookmin/stream/ApiUser.java b/api/common-api/src/main/java/kr/ac/kookmin/stream/ApiUser.java new file mode 100644 index 0000000..4581ff9 --- /dev/null +++ b/api/common-api/src/main/java/kr/ac/kookmin/stream/ApiUser.java @@ -0,0 +1,9 @@ +package kr.ac.kookmin.stream; + +/** + * 인증된 사용자의 스냅샷. 각 클라이언트 모듈이 role별 구현체를 두고 ArgumentResolver가 컨트롤러 파라미터로 주입한다. + */ +public interface ApiUser { + + Long userId(); +} diff --git a/api/common-api/src/main/java/kr/ac/kookmin/stream/ApiUserArgumentResolver.java b/api/common-api/src/main/java/kr/ac/kookmin/stream/ApiUserArgumentResolver.java new file mode 100644 index 0000000..630c01a --- /dev/null +++ b/api/common-api/src/main/java/kr/ac/kookmin/stream/ApiUserArgumentResolver.java @@ -0,0 +1,12 @@ +package kr.ac.kookmin.stream; + +import org.springframework.web.method.support.HandlerMethodArgumentResolver; + +/** + * {@link ApiUser} 구현체를 컨트롤러 파라미터로 주입하는 리졸버. + *

+ * {@link WebMvcConfig}가 이 타입의 빈만 수집해 등록한다. {@code HandlerMethodArgumentResolver}를 그대로 수집하면 + * 컨텍스트에 등록된 무관한 리졸버까지 딸려오므로 전용 타입으로 좁힌다. + */ +public interface ApiUserArgumentResolver extends HandlerMethodArgumentResolver { +} diff --git a/api/common-api/src/main/java/kr/ac/kookmin/stream/WebMvcConfig.java b/api/common-api/src/main/java/kr/ac/kookmin/stream/WebMvcConfig.java new file mode 100644 index 0000000..4635233 --- /dev/null +++ b/api/common-api/src/main/java/kr/ac/kookmin/stream/WebMvcConfig.java @@ -0,0 +1,28 @@ +package kr.ac.kookmin.stream; + +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +/** + * 클라이언트 모듈이 등록한 {@link ApiUserArgumentResolver}를 일괄 등록한다. + *

+ * 리졸버를 타입으로 지목하지 않고 주입받아, common-api가 app-api·admin-api를 컴파일 타임에 참조하지 않도록 한다. + * 클라이언트 모듈이 추가돼도 이 클래스는 수정할 필요가 없다. + */ +@Configuration +@RequiredArgsConstructor +public class WebMvcConfig implements WebMvcConfigurer { + + private final ObjectProvider apiUserArgumentResolvers; + + @Override + public void addArgumentResolvers(List resolvers) { + // 생성자가 아니라 이 시점에 펼쳐, WebMvcConfigurer 생성 단계에서 리졸버 빈을 앞당겨 만들지 않는다. + // 리졸버 빈이 하나도 없는 컨텍스트에서도 orderedStream()은 빈 스트림이라 기동이 깨지지 않는다. + resolvers.addAll(apiUserArgumentResolvers.orderedStream().toList()); + } +} From afc1764e7cbd88da1ef7f1dabff37ecb88258fa2 Mon Sep 17 00:00:00 2001 From: sangrae Date: Fri, 11 Sep 2026 00:21:05 +0900 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20app-api=EC=97=90=20AppApiUser=20?= =?UTF-8?q?=EC=A3=BC=EC=9E=85=20=EB=A6=AC=EC=A1=B8=EB=B2=84=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kr/ac/kookmin/stream/app/AppApiUser.java | 26 +++++++++++ .../resolver/AppApiUserArgumentResolver.java | 44 +++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 api/app-api/src/main/java/kr/ac/kookmin/stream/app/AppApiUser.java create mode 100644 api/app-api/src/main/java/kr/ac/kookmin/stream/app/resolver/AppApiUserArgumentResolver.java diff --git a/api/app-api/src/main/java/kr/ac/kookmin/stream/app/AppApiUser.java b/api/app-api/src/main/java/kr/ac/kookmin/stream/app/AppApiUser.java new file mode 100644 index 0000000..573cc7f --- /dev/null +++ b/api/app-api/src/main/java/kr/ac/kookmin/stream/app/AppApiUser.java @@ -0,0 +1,26 @@ +package kr.ac.kookmin.stream.app; + +import kr.ac.kookmin.stream.ApiUser; +import lombok.Getter; +import lombok.experimental.Accessors; + +/** + * 학생 앱 요청의 인증 사용자. 컨트롤러가 파라미터로 선언하면 AppApiUserArgumentResolver가 주입한다. + *

+ * record가 아니라 생성자를 감춘 클래스로 둔다. 리졸버가 없는 컨텍스트에서 스프링의 폴백 + * ServletModelAttributeMethodProcessor가 이 타입을 요청 파라미터로 생성하지 못하게 막기 위함이다. + */ +@Getter +@Accessors(fluent = true) +public final class AppApiUser implements ApiUser { + + private final Long userId; + + private AppApiUser(Long userId) { + this.userId = userId; + } + + public static AppApiUser from(Long userId) { + return new AppApiUser(userId); + } +} diff --git a/api/app-api/src/main/java/kr/ac/kookmin/stream/app/resolver/AppApiUserArgumentResolver.java b/api/app-api/src/main/java/kr/ac/kookmin/stream/app/resolver/AppApiUserArgumentResolver.java new file mode 100644 index 0000000..bf07281 --- /dev/null +++ b/api/app-api/src/main/java/kr/ac/kookmin/stream/app/resolver/AppApiUserArgumentResolver.java @@ -0,0 +1,44 @@ +package kr.ac.kookmin.stream.app.resolver; + +import kr.ac.kookmin.stream.ApiUserArgumentResolver; +import kr.ac.kookmin.stream.app.AppApiUser; +import kr.ac.kookmin.stream.common.BusinessException; +import kr.ac.kookmin.stream.common.CommonErrorCode; +import kr.ac.kookmin.stream.common.PrincipalProvider; +import kr.ac.kookmin.stream.common.Role; +import lombok.RequiredArgsConstructor; +import org.springframework.core.MethodParameter; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.ModelAndViewContainer; + +/** + * {@link AppApiUser} 파라미터를 주입한다. + *

+ * SecurityConfig가 {@code /v1/app/**}를 STUDENT로 이미 1차 인가하므로 여기서의 role 검사는 2차 방어선이다. + */ +@Component +@RequiredArgsConstructor +public class AppApiUserArgumentResolver implements ApiUserArgumentResolver { + + private final PrincipalProvider principalProvider; + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return AppApiUser.class.equals(parameter.getParameterType()); + } + + @Override + public AppApiUser resolveArgument( + MethodParameter parameter, + ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, + WebDataBinderFactory binderFactory + ) { + if (!principalProvider.roles().contains(Role.STUDENT)) { + throw new BusinessException(CommonErrorCode.FORBIDDEN); + } + return AppApiUser.from(principalProvider.userId()); + } +} From 3354727fdc5617a46cd11f6565d104dbacd80c51 Mon Sep 17 00:00:00 2001 From: sangrae Date: Fri, 11 Sep 2026 00:21:05 +0900 Subject: [PATCH 3/3] =?UTF-8?q?feat:=20admin-api=EC=97=90=20AdminApiUser?= =?UTF-8?q?=20=EC=A3=BC=EC=9E=85=20=EB=A6=AC=EC=A1=B8=EB=B2=84=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ac/kookmin/stream/admin/AdminApiUser.java | 26 +++++++++++ .../AdminApiUserArgumentResolver.java | 44 +++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 api/admin-api/src/main/java/kr/ac/kookmin/stream/admin/AdminApiUser.java create mode 100644 api/admin-api/src/main/java/kr/ac/kookmin/stream/admin/resolver/AdminApiUserArgumentResolver.java diff --git a/api/admin-api/src/main/java/kr/ac/kookmin/stream/admin/AdminApiUser.java b/api/admin-api/src/main/java/kr/ac/kookmin/stream/admin/AdminApiUser.java new file mode 100644 index 0000000..c8d7f13 --- /dev/null +++ b/api/admin-api/src/main/java/kr/ac/kookmin/stream/admin/AdminApiUser.java @@ -0,0 +1,26 @@ +package kr.ac.kookmin.stream.admin; + +import kr.ac.kookmin.stream.ApiUser; +import lombok.Getter; +import lombok.experimental.Accessors; + +/** + * 운영진 콘솔 요청의 인증 사용자. 컨트롤러가 파라미터로 선언하면 AdminApiUserArgumentResolver가 주입한다. + *

+ * record가 아니라 생성자를 감춘 클래스로 둔다. 리졸버가 없는 컨텍스트에서 스프링의 폴백 + * ServletModelAttributeMethodProcessor가 이 타입을 요청 파라미터로 생성하지 못하게 막기 위함이다. + */ +@Getter +@Accessors(fluent = true) +public final class AdminApiUser implements ApiUser { + + private final Long userId; + + private AdminApiUser(Long userId) { + this.userId = userId; + } + + public static AdminApiUser from(Long userId) { + return new AdminApiUser(userId); + } +} diff --git a/api/admin-api/src/main/java/kr/ac/kookmin/stream/admin/resolver/AdminApiUserArgumentResolver.java b/api/admin-api/src/main/java/kr/ac/kookmin/stream/admin/resolver/AdminApiUserArgumentResolver.java new file mode 100644 index 0000000..acd1049 --- /dev/null +++ b/api/admin-api/src/main/java/kr/ac/kookmin/stream/admin/resolver/AdminApiUserArgumentResolver.java @@ -0,0 +1,44 @@ +package kr.ac.kookmin.stream.admin.resolver; + +import kr.ac.kookmin.stream.ApiUserArgumentResolver; +import kr.ac.kookmin.stream.admin.AdminApiUser; +import kr.ac.kookmin.stream.common.BusinessException; +import kr.ac.kookmin.stream.common.CommonErrorCode; +import kr.ac.kookmin.stream.common.PrincipalProvider; +import kr.ac.kookmin.stream.common.Role; +import lombok.RequiredArgsConstructor; +import org.springframework.core.MethodParameter; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.ModelAndViewContainer; + +/** + * {@link AdminApiUser} 파라미터를 주입한다. + *

+ * SecurityConfig가 {@code /v1/admin/**}를 ADMIN으로 이미 1차 인가하므로 여기서의 role 검사는 2차 방어선이다. + */ +@Component +@RequiredArgsConstructor +public class AdminApiUserArgumentResolver implements ApiUserArgumentResolver { + + private final PrincipalProvider principalProvider; + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return AdminApiUser.class.equals(parameter.getParameterType()); + } + + @Override + public AdminApiUser resolveArgument( + MethodParameter parameter, + ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, + WebDataBinderFactory binderFactory + ) { + if (!principalProvider.roles().contains(Role.ADMIN)) { + throw new BusinessException(CommonErrorCode.FORBIDDEN); + } + return AdminApiUser.from(principalProvider.userId()); + } +}