common: MSA 서비스 권한 체크 - takeoff-26/logistics-service GitHub Wiki
라우팅을 진행하는 GateWay에서 인증 후 헤더에 로그인된 사용자의 정보 중 Id와 ROLE 값을 X-User-Id, X-User-Role로 넣어주게 된다. 각 라우팅된 요청에는 헤더에 이 값을 포함해서 서비스에 분배 되는데, 각 서비스는 security 의존성을 가지고 있지 않으니 권한 검사를 별도로 진행해야 하는 부분에서 어노테이션을 통해 관리하기로 하고 요청에서 헤더 값을 추출해 바인딩하는 로직을 구현하고자 했다.
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface UserInfo {
}
public record UserInfoDto(Long userId, UserRole role) {
public static UserInfoDto of(String userId, String role) {
if(userId == null || role == null) {
return empty();
}
return parseUserInfo(userId, role);
}
public static UserInfoDto empty() {
return new UserInfoDto(null, null);
}
private static UserInfoDto parseUserInfo(String userId, String role) {
try {
return new UserInfoDto(Long.parseLong(userId), UserRole.valueOf(role));
} catch (IllegalArgumentException e) {
return empty();
}
}
}
public enum UserRole {
MASTER_ADMIN,
HUB_MANAGER,
COMPANY_MANAGER,
HUB_DELIVERY_MANAGER,
COMPANY_DELIVERY_MANAGER,
}
@Component
public class UserInfoArgumentResolver implements HandlerMethodArgumentResolver {
private static final String USER_ID_HEADER = "X-User-Id";
private static final String USER_ROLE_HEADER = "X-User-Role";
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(UserInfo.class)
&& parameter.getParameterType().equals(UserInfoDto.class);
}
@Override
public Object resolveArgument(MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) {
return Optional.of(webRequest.getNativeRequest())
.filter(HttpServletRequest.class::isInstance)
.map(HttpServletRequest.class::cast)
.map(this::extractUserInfo)
.orElse(UserInfoDto.empty());
}
private UserInfoDto extractUserInfo(HttpServletRequest request) {
return UserInfoDto.of(
request.getHeader(USER_ID_HEADER),
request.getHeader(USER_ROLE_HEADER));
}
}
AgumentResolver를 구현해서 Request 속 사용자 정보가 들어 있는 헤더를 꺼내 별도의 클래스를 만들어 저장 시키고 공통 모듈에 구성해서 의존성을 주입받는 서비스 들에서 어노테이션을 통해 사용자 정보를 서비스 로직에서 사용할 수 있게끔 구현했다. 별도로 권한에 대해서 헤더 값이 들어 올텐데 이 헤더 값을 기준으로 권한 검사를 하는 어노테이션도 구성했다.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RoleCheck {
UserRole[] roles() default {UserRole.MASTER_ADMIN, UserRole.COMPANY_MANAGER, UserRole.HUB_MANAGER,
UserRole.COMPANY_DELIVERY_MANAGER, UserRole.HUB_DELIVERY_MANAGER};
}
@Aspect
@Component
public class RoleCheckAspect {
private static final String USER_ROLE_HEADER = "X-User-Role";
@Before("@annotation(roleCheck)")
public void roleCheck(RoleCheck roleCheck) {
Set<UserRole> roles = Set.of(roleCheck.roles());
UserRole userRole = getUserRole();
if (!roles.contains(userRole)) {
throw BusinessException.from(CommonErrorCode.FORBIDDEN);
}
}
private UserRole getUserRole() {
ServletRequestAttributes attributes =
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes == null) {
throw BusinessException.from(CommonErrorCode.UNAUTHORIZED);
}
HttpServletRequest request = attributes.getRequest();
String roleHeader = request.getHeader(USER_ROLE_HEADER);
if (roleHeader == null || roleHeader.isEmpty()) {
throw BusinessException.from(CommonErrorCode.UNAUTHORIZED);
}
return UserRole.valueOf(roleHeader);
}
}
만들어 둔 Role 값을 토대로 request에 담긴 X-User-Role에서 role 값을 꺼내 검사하는 어노테이션을 구성했다. 각 컨트롤러에 @PreAuthorize 처럼 사용하게끔 구성했다.