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 처럼 사용하게끔 구성했다.

⚠️ **GitHub.com Fallback** ⚠️