본문으로 바로가기

프로젝트의 전체 소스 코드는 이곳에서 확인하실 수 있습니다.


 

이전에 포스팅한 Spring] 커스텀 어노테이션으로 로그인 확인과 현재 로그인된 사용자 정보 불러오기에 추가적으로 회원 권한에 따른 특정 핸들러에 접근 제한을 설정하는 방법을 알아보겠습니다.

권한에 따른 접근 제한이 필요한 경우

현재 진행하고 있는 프로젝트에서는 총 3개의 UserLevel 존재한다.

  1. 회원가입 후 이메일 인증을 완료하지 않은 회원 - 일부 서비스 이용 제한 UNAUTH
  2. 회워가입 후 이메일 인증을 완료한 회원 - 관리자 기능 외 모든 서비스 이용 가능 AUTH
  3. 관리자 - 모든 서비스 이용 가능 ADMIN

회원 가입 후 아래 이미지 처럼 임의의 토큰을 포함하는 URL이 전송되며, 해당 링크를 클릭하면 인증이 완료된다. 번거롭지만 이메일 인증 기능을 추가한 이유는 현금 거래가 이루어지는 서비스에서 무분별한 회원가입을 방지하여 유령회원을 구분하는 기능은 필수라고 생각하기 때문이다.

 

회원가입 인증 비즈니스 로직은 다음과 같다.

  1. 회원가입시 랜덤 토큰을 생성하여 입력한 email로 인증 링크를 발송한다. 아래 이미지와 같이 이메일로 전송된 링크를 클릭하면 인증이 완료되는 방식이다.

 

  1. 만약 회원가입 후 인증을 하지 않은 경우라면 해당 프로젝트의 핵심 기능인 판매/구매와 관련된 기능을 제한한다.

회원가입 인증 여부에 따른 기능 제한은 지난번에 포스팅했던 글(바로가기)에 조금만 수정해주면 간단하게 처리할 수 있다.

권한 설정 과정

우선 지난시간에 구현한 LoginCheck 어노테이션은 다음과 같다.

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

/**
 * @LoginCheck :  현재 사용자가 로그인 한 사용자인지 확인한다.
 * @Retention : 어느 시점까지 어노테이션의 메모리를 가져갈 지 설정
 * @Target : 어노테이션이 사용될 위치를 지정한다.
 */

@Retention(RUNTIME)
@Target(METHOD)
public @interface LoginCheck {

   UserLevel authority() default UserLevel.UNAUTH;
}

default를 UNAUTH로 설정함으로써 권한을 명시하지 않으면 리소스의 접근 권한이 UNAUTH로 설정된다.

추가적으로 프로젝트에서 사용할 3개의 권한을 가지는 enum 클래스를 구현한다.

public enum UserLevel {
    UNAUTH, AUTH, ADMIN
}

지난시간에 작성했던 LoginCheckInterceptor에 권한을 확인하는 코드를 아래와 같이 추가하면 된다.



/**
 * HandlerMethod  : 실행될 핸들러(컨트롤러의 메소드) loginCheck가 null이라면 로그인 없이 접근가능한 핸들러이므로 true 리턴
 */

@Component
@RequiredArgsConstructor
public class LoginCheckInterceptor implements HandlerInterceptor {

    private final SessionLoginService sessionLoginService;

    @Inject
    private Environment environment;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
        Object handler)
        throws Exception {

        if (handler instanceof HandlerMethod) {
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            LoginCheck loginCheck = handlerMethod.getMethodAnnotation(LoginCheck.class);

            // LoginCheck 어노테이션이 없다면, 해당 리소스에 대한 접근 제한이 없으므로 통과시킨다.
            if (loginCheck == null) {
                return true;
            }

            // 로그인이 필요한 리소스에 로그인 없이 접근했다면, 예외를 발생시킨다.
            if (sessionLoginService.getLoginUser() == null) {
                throw new UnauthenticatedUserException("로그인 후 이용 가능합니다.");
            }


            UserLevel auth = loginCheck.authority();

            switch (auth) {
                case ADMIN:
                    adminUserLevel();
                    break;

                case AUTH:
                    authUserLevel();
                    break;
            }
        }
        return true;
    }

    /**
     * 현재 USER의 권한(UserLevel)이 AUTH인지 확인한다. 해당 리소스는 ADMIN과 AUTH만 접근 가능하다. 따라서 UNAUTH인 경우에만 제한한다.
     */
    private void authUserLevel() {
        UserLevel auth = sessionLoginService.getUserLevel();
        if (auth == UserLevel.UNAUTH) {
            throw new NotAuthorizedException("해당 리소스에 대한 접근 권한이 존재하지 않습니다.");

        }
    }

    /**
     * 현재 USER의 권한(UserLevel)이 ADMIN인지 확인한다. ADMIN이 아니라면 해당 요청을 수행할 수 없다.
     */
    private void adminUserLevel() {
        UserLevel auth = sessionLoginService.getUserLevel();
        if (auth != UserLevel.ADMIN) {
            throw new NotAuthorizedException("해당 리소스에 대한 접근 권한이 존재하지 않습니다.");
        }
    }
}

여기서 주의할 점은 권한 검사 외에도 계층 처리를 하는 코드를 추가로 구현해야 한다.

@LoginCheck 어노테이션의 경우, 권한이 default 값인 UNAUTH로 설정된다. 즉 이메일 인증 없이 리소스에 접근할 수 있다는 뜻이다.

이러한 경우 따로 계층 처리를 할 필요가 없지만, @LoginCheck(authority = UserLevel.AUTH)의 경우 계층 처리가 필요하다.

 

@LoginCheck(authority = UserLevel.AUTH) 가 적용된 핸들러는 UNAUTH만 접근이 불가능하고 ADMINAUTH는 접근이 가능해야 한다. 따라서  switch문을 통해 계층검사를 진행하는 코드를 추가하였다.

적용

로그인을 확인하는 어노테이션에 추가적으로ADMIN권한을 부여함으로써 Interceptor에서 해당 사용자의 이메일 인증 여부를 확인한다.

@LoginCheck(authority = UserLevel.ADMIN)
@PostMapping
public ResponseEntity<Void> createBrand(@Valid @RequestBody SaveRequest requestDto) {
    brandService.saveBrand(requestDto);
         return CREATED;
    }