개발블로그

Authentication/Authorization 기능 구현(1) - interceptor vs spring security 본문

Spring

Authentication/Authorization 기능 구현(1) - interceptor vs spring security

개발자수니 2019. 1. 3. 00:46

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

 

A/A기능을 구현하기 위해서 Spring-security가 정교화되기 전까지는 직접 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 이후의 흐름인데, 설명은 생략하겠습니다.

 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를 만들었습니다.

1
2
3
4
5
6
7
8
9
10
11
/**
 * Login이 필요한 요청일 경우 사용한다.
 * @author leesujin
 *
 */
@Target({ElementType.METHOD,ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LoginRequired {
 
}
cs
1
2
3
4
5
6
7
8
9
10
11
12
/**
 * Admin권한이 있는 유저만 접근 가능하다.
 * @author leesujin
 *
 */
@Target({ElementType.METHOD,ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@LoginRequired
public @interface AdminOnly {
 
}
cs
 

@AdminOnly의 경우는 인증+권한체크를 함께 하기 위해 @LoginRequired라는 어노테이션을 달아주었습니다. @LoginRequired 어노테이션을 다른 어노테이션 타입에 추가할 수 있도록 하기 위해서는 6라인 @Target에 ElementType.ANNOTATION_TYPE을 추가해주어야 합니다.  

 

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

1
2
3
4
5
6
7
8
@Controller
public class AdminController {
    @GetMapping("/admin/home")
    @AdminOnly
    public String adminHome() {
        return "admin/home";
    }
}
cs

 

1
2
3
4
5
6
7
8
9
10
11
12
13
@Controller
public class HomeController {
    @GetMapping("/home")
    @LoginRequired
    public String homePage() {
        return "/home/home";
    }
    
    @GetMapping("/main")
    public String mainPage() {
        return "/home/main";
    }
}
cs
이제 LoginInterceptor를 살펴보겠습니다.
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@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);
    }
}
cs

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

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

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

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

 

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

1
2
3
4
5
6
7
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor());
    }
}
cs
다음 포스팅에서는 이 같은 기능을 Spring-security를 이용해 구현해보도록 하겠습니다. 

 

전체 코드는 Github에서 확인하실 수 있습니다.

 

Comments