Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package kr.ac.kookmin.stream.admin;

import kr.ac.kookmin.stream.ApiUser;
import lombok.Getter;
import lombok.experimental.Accessors;

/**
* 운영진 콘솔 요청의 인증 사용자. 컨트롤러가 파라미터로 선언하면 AdminApiUserArgumentResolver가 주입한다.
* <p>
* 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);
}
}
Original file line number Diff line number Diff line change
@@ -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} 파라미터를 주입한다.
* <p>
* 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());
}
}
26 changes: 26 additions & 0 deletions api/app-api/src/main/java/kr/ac/kookmin/stream/app/AppApiUser.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package kr.ac.kookmin.stream.app;

import kr.ac.kookmin.stream.ApiUser;
import lombok.Getter;
import lombok.experimental.Accessors;

/**
* 학생 앱 요청의 인증 사용자. 컨트롤러가 파라미터로 선언하면 AppApiUserArgumentResolver가 주입한다.
* <p>
* 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);
}
}
Original file line number Diff line number Diff line change
@@ -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} 파라미터를 주입한다.
* <p>
* 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());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package kr.ac.kookmin.stream;

/**
* 인증된 사용자의 스냅샷. 각 클라이언트 모듈이 role별 구현체를 두고 ArgumentResolver가 컨트롤러 파라미터로 주입한다.
*/
public interface ApiUser {

Long userId();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package kr.ac.kookmin.stream;

import org.springframework.web.method.support.HandlerMethodArgumentResolver;

/**
* {@link ApiUser} 구현체를 컨트롤러 파라미터로 주입하는 리졸버.
* <p>
* {@link WebMvcConfig}가 이 타입의 빈만 수집해 등록한다. {@code HandlerMethodArgumentResolver}를 그대로 수집하면
* 컨텍스트에 등록된 무관한 리졸버까지 딸려오므로 전용 타입으로 좁힌다.
*/
public interface ApiUserArgumentResolver extends HandlerMethodArgumentResolver {
}
Original file line number Diff line number Diff line change
@@ -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}를 일괄 등록한다.
* <p>
* 리졸버를 타입으로 지목하지 않고 주입받아, common-api가 app-api·admin-api를 컴파일 타임에 참조하지 않도록 한다.
* 클라이언트 모듈이 추가돼도 이 클래스는 수정할 필요가 없다.
*/
@Configuration
@RequiredArgsConstructor
public class WebMvcConfig implements WebMvcConfigurer {

private final ObjectProvider<ApiUserArgumentResolver> apiUserArgumentResolvers;

@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
// 생성자가 아니라 이 시점에 펼쳐, WebMvcConfigurer 생성 단계에서 리졸버 빈을 앞당겨 만들지 않는다.
// 리졸버 빈이 하나도 없는 컨텍스트에서도 orderedStream()은 빈 스트림이라 기동이 깨지지 않는다.
resolvers.addAll(apiUserArgumentResolvers.orderedStream().toList());
}
}
Loading