Spring Interceptor를 이용한 권한 체크

개발하는 구황작물·2023년 9월 26일
0

회사 프로젝트

목록 보기
3/8

회사 프로젝트 진행 중 권한 체크를 효율적으로 할 수 있을지에 대해 고민한 과정을 정리해보았다.

기존 상황

회사 요구사항을 보니 Role이 5개가 있었다.

표로 정리해서 보여주자면

권한A_RoleB_RoleC_RoleD_Role...
a권한oxxx
b권한ooxx
c권한oxox
d권한oooo
..................

위와 같은 식으로 각각 다른 권한범위를 가지고 있다.

요구사항에 따라 필요한 인증 및 권한 직업은

  • 로그인이 필요없는 API
  • 로그인이 필요한 API
  • 특정 권한을 가지고 있는지 체크하는 API

다음과 같은 종류의 작업이 필요했다.(차후에 더 변경될 수 있었음)

기존의 작업방식

지금까지의 권한 체크는 각각의 서비스 계층에서 발생하고 있었다.

AService.java

public void 권한 체크가 필요한 메서드(..., String email) {
	MemberRole memberRole = memberFindService.findByEmail(email).getRole();
    validateMemberRole(memberRole);
	...
}

이러한 방식은 중복되는 코드를 만들어내고 하나의 메서드가 여러 역할을 가지게 하여 단일 책임 원칙(SRP)에도 위반되고 있었다.

Interceptor 활용

Interceptor는 Dispatcher Servlet이 컨트롤러를 호출하기 전/후에 적용된다.

그 덕분에 스프링 컨텍스트 내부에서 Controller에 관한 요청, 응답을 처리할 수 있다.

Interceptor를 활용하면 중복되는 코드를 없에고 혹시 모를 코드 누락 가능성을 없엘 수 있어서 Interceptor를 활용하기로 하였다.

(AOP도 후보군에 고려를 하였으나 컨트롤러는 각각 사용하는 파라미터나 리턴 값이 다 달라서 AOP 보단 Interceptor를 사용하는게 낫다고 판단이 들었다. AOP를 사용한 로깅 시스템은 다른 포스팅에서 준비해보겠다.)

코드

  1. @CheckRole
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckRole {
    Role[] role() default {Role.MEMBER};

    enum Role {
        DIVISION_MANAGER("Division Manager", 1)
        , DIVISION_OBSERVER("Division Observer",2)
        , TEAM_MANAGER("Team Manager",3)
        , TEAM_OBSERVER("Team Observer",4)
        , MEMBER("member",5);

        @Getter
        private String role;
        @Getter
        private Integer number;

        Role(String role, Integer number) {
            this.role = role;
            this.number = number;
        }

        public static int checkUserRoleNumber(String role) {

            return Arrays.stream(Role.values())
                    .filter(r -> r.role.equals(role))
                    .findFirst()
                    .orElseThrow(() -> new BusinessLogicException(BusinessErrorCode.MEMBER_MEMBER_ROLE_NOT_FOUND))
                    .getNumber();
        }
    }
}

일단 컨트롤러 메서드에 부착할 어노테이션을 만들었다.

API에 접근할 수 있는 역할이 여러 개일 수 있으므로 ROLE[] 배열로 지정하였고

아래에는 enum Role과 Role을 찾아낼 수 있는 로직을 만들어냈다.

  1. CheckRoleInterceptor
@Slf4j
@RequiredArgsConstructor
@Component
public class CheckRoleInterceptor implements HandlerInterceptor {
    private final MemberMemberRoleFindService memberMemberRoleFindService;

	//동작 이전에 권한 체크를 위해 preHandle 사용
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        log.info("# interceptor start");
        if(!(handler instanceof HandlerMethod)) {
            return true;
        }
        // Controller에 있는 메서드인지 확인하기 위해 HandlerMethod 타입인지 체크

        HandlerMethod handlerMethod = (HandlerMethod) handler;

        CheckRole checkRole = handlerMethod.getMethodAnnotation(CheckRole.class);
        if(checkRole == null) return true;
        //@checkRole 이 부착되지 않았다면 true 리턴

        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if(!authentication.isAuthenticated()) return false;
        // 로그인 하지 않았다면 false 리턴

        String email = authentication.getName();
        log.info("# email={}", email);
        MemberMemberRoleDto.CheckMemberRoleDto checkMemberRoleDto = memberMemberRoleFindService.selectMemberRoleByEmail(email);
        log.info("# checkMemberRoleDto = {}", checkMemberRoleDto);
        if(Objects.equals(checkMemberRoleDto.getRole(), "SM")) return true;
        //시스템 매니저(Admin)이라면 무조건 통과

        String userRole = checkMemberRoleDto.getMemberRole();
        int roleNumber = CheckRole.Role.checkUserRoleNumber(userRole);

        log.info("# userRole = {}, roleNumber={}", userRole, roleNumber);

        boolean checkUserRole = false;
        for (CheckRole.Role role : checkRole.role()) {
            if(role.compareTo(DIVISION_MANAGER) == 0) {
                if(userRole.equals(DIVISION_MANAGER.getRole())) checkUserRole = true;
                break;
            }
            if(role.compareTo(DIVISION_OBSERVER) == 0) {
                if(userRole.equals(DIVISION_OBSERVER.getRole())) checkUserRole = true;
                break;
            }
            if(role.compareTo(TEAM_MANAGER) == 0) {
                if(userRole.equals(TEAM_MANAGER.getRole())) checkUserRole = true;
                break;
            }
            if(role.compareTo(TEAM_OBSERVER) == 0) {
                if(userRole.equals(TEAM_OBSERVER.getRole())) checkUserRole = true;
                break;
            }
            if(role.compareTo(MEMBER) == 0) {
                if(userRole.equals(MEMBER.getRole())) checkUserRole = true;
                break;
            }
        }
        return checkUserRole;
        //어노테이션에 있는 ROLE 중 User가 하나라도 해당이 된다면 통과
    }
}
  1. WebConfig
@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
    private final CheckRoleInterceptor checkRoleInterceptor;

    @Override //어노테이션 적용
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(checkRoleInterceptor);
    }
}
profile
어쩌다보니 개발하게 된 구황작물

0개의 댓글