본문 바로가기
Auth (Authentication & Authorization)

Interceptor를 이용한 Auth 구현

by kellis 2020. 10. 12.

 

interceptor vs spring security

 

앞서 언급하였듯이, 대부분의 웹 애플리케이션은 Authentication(인증)/Authorization(권한) 기능을 구현하고 있습니다. 이 과정에 필요한 인증된 사용자 정보를 저장하기 위해 Session 혹은 JWT(JSON Web Token) 등을 이용할 수 있는데, 이 포스트에서는 Session에 저장한다고 가정하겠습니다. 

 

Spring-security가 정교화되기 전까지는 A/A 기능을 직접 Session에 접근하여 조작했습니다.

(Spring-security도 내부적으로는 Session을 이용하지만 out-of-box로 구현되어 있어, 제공되는 정형화된 API를 이용해 A/A를 구현할 수 있습니다. 뿐만 아니라 보안적인 이슈까지도 처리할 수 있습니다)

 

이 포스트와 다음 포스트에서는 Spring-security를 미사용/사용하여 그 둘의 구조를 비교해보겠습니다. 

 

 

[1] Spring MVC Lifecycle

 

구현하기에 앞서 Request의 흐름을 파악하기 위해 Spring MVC의 Lifecycle을 먼저 살펴보겠습니다.

  •  1 : 브라우저로부터 요청이 들어오면 Servlet Container가 생성한 Dispatcher Servlet이 그 요청을 가로챔.
  •  2,3 : Dispatcher Servlet은 그 요청을 가지고 Handler Mapping에게 해당 요청을 어느 Controller method에게 위임할지 결정.
  •  4 : Dispatcher Servlet은 실행할 Controller method정보(HandlerMethod)를 Handler Adapter에게 전달. Handler Adapter는 전달받은 Controller method를 실행하는데, 실행하기 전에 HandlerInterceptorAdapter를 구현한 interceptor들을 먼저 실행. (1~9 은 Handler Adapter 이후의 흐름으로, Spring MVC document를 참고)
  •  5 : 결과적으로 ViewName과 Model을 반환. 
  •  6,7 : Dispatcher Servlet은 Handler Adapter로부터 응답받은 ViewName과 Model을 View Resolver에게 위임하여, response body가 될 view(html)를 응답받음. 

 

Handler Adapter가 Controller method 혹은 interceptor를 실행하는 도중 HttpSession을 이용해야 한다면, Servlet Container가 Session storage를 확인하여 session을 새로 발급하거나 기존의 session을 매핑시켜줍니다.

 

 

[2] LoginInterceptor 구현 

 

이 포스트에서는, Spring-security를 이용하지 않고 A/A 기능을 구현하기 위해 HandlerInterceptorAdapter를 구현한 LoginInterceptor를 작성했습니다. LoginInterceptor의 프로세스는 다음과 같습니다. 

  1. 실행하려는 Controller method에 대해 인증이 필요한지 확인한다.
  2. 인증이 필요하지 않다면 바로 Controller를 실행한다.
  3. 인증이 필요하다면 HttpSession에 저장된 데이터를 확인한다. HttpSession에 저장된 데이터가 없다면 Login이 필요하다는 것을 의미하고 이미 저장된 데이터가 있다면 Controller method를 실행한다.

 

LoginInterceptor의 코드를 살펴보기 앞서, Controller method마다 인증 여부와 권한 제어를 구분하기 위해 만든 Custom annotation을 보겠습니다. 

 

인증 여부를 위해 @LoginRequired권한 제어를 위해 @AdminOnly를 만들었습니다.

/**
 * Login이 필요한 요청일 경우 사용한다.
 * @author leesujin
 *
 */
@Target({ElementType.METHOD,ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LoginRequired {
  
}
/**
 * Admin권한이 있는 유저만 접근 가능하다.
 * @author leesujin
 *
 */
@Target({ElementType.METHOD,ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@LoginRequired
public @interface AdminOnly {
  
}
  • @AdminOnly의 경우는 인증과 권한 체크를 모두 해주어야 하므로 앞서 생성한 @LoginRequired 어노테이션 달아줍니다. 
  • @LoginRequired 어노테이션을 다른 어노테이션 타입에 추가할 수 있도록 하기 위해서는 6라인 @Target에 ElementType.ANNOTATION_TYPE을 추가해주어야 합니다.

 

그리고 필요에 따라 Controller의 method에 해당 어노테이션을 추가해줍니다.

@Controller
public class AdminController {
    @GetMapping("/admin/home")
    @AdminOnly
    public String adminHome() {
        return "admin/home";
    }
}
@Controller
public class HomeController {
    @GetMapping("/home")
    @LoginRequired
    public String homePage() {
        return "/home/home";
    }
     
    @GetMapping("/main")
    public String mainPage() {
        return "/home/main";
    }
}

 

이제 LoginInterceptor를 살펴보겠습니다. 

@Configuration
public class LoginInterceptor extends HandlerInterceptorAdapter {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {
        if (handler instanceof HandlerMethod) {
            HandlerMethod hm = (HandlerMethod) handler;
            User sessionUser = (User) request.getSession().getAttribute("USER");
            if (hm.hasMethodAnnotation(LoginRequired.class) && sessionUser == null) {
                throw new AuthenticationException(request.getRequestURI());
            }
            if(hm.hasMethodAnnotation(AdminOnly.class) && sessionUser.getAuthority() != "ADMIN") {
                throw new AuthorizationException();
            }
        }
        return super.preHandle(request, response, handler);
    }
}

Controller의 method에 매핑된 URL이 요청되었을 경우, 위 코드가 실행됩니다. DispatcherServlet이 HandlerAdapter에게 Controller method 정보 즉, HandlerMethod를 넘겨주기 때문입니다. 따라서 만약 CORS 때문에 OPTIONS 요청이 온다거나, 리소스 요청이 온다면 이 handler는 HandlerMethod 타입이 아닐뿐더러 LoginInterceptor를 실행할 필요도 없습니다. 그러므로 HandlerMethod 타입인지 확인하지 않는다면, 캐스팅하는 과정에서 에러가 발생합니다. 

 

6,7: 매개변수로 전달받은 handler가 HandlerMethod 타입인지 체크해주어야 하고, HandlerMethod 타입으로 캐스팅해야 함.

9: 실행하고자 하는 Controller method의 어노테이션 중 LoginRequired가 있는지 체크. @LoginRequired가 추가되어있다면, 인증이 필요한 method이므로 session정보가 있는지 확인해야 함. session정보가 비어있으면 로그인되어있지 않은 상태이므로 예외를 발생시킴. 

12: 실행하고자 하는 Controller method의 어노테이션 중 AdminOnly가 있는지 체크. @AdminOnly가 추가되어있다면, 관리자만 접근할 수 있는 method이므로 session에 저장한 User의 정보를 확인하여 ADMIN 권한이 없다면 예외를 발생시킴. 

 

마지막으로 이 LoginInterceptor를 InterceptorRegistry에 추가하면, Controller method를 실행하기 전에 LoginInterceptor를 실행하게 될 것입니다.

@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor());
    }
}

 

다음 포스팅에서는 이 같은 기능을 Spring-security를 이용해 구현해보도록 하겠습니다. 전체 코드는 Github에서 확인하실 수 있습니다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

댓글