인증과 인가?

인증 : 사용자의 신원을 검증하는 프로세스, ID와 PW를 이용해 로그인 하는 것이 인증이라고 할 수 있다.

인가 : 인증된 사용자가 어떤 자원에 접근 권한을 가지고 있는지 확인하는 절차

 

지난 강의에서 세션을 이용해 인증 기능을 구현했지만, 사용자가 url만 안다면 로그인 없이 모든 화면과 기능을 사용할 수 있었다.

이번 강의에선 필터와 스프링 인터셉터를 사용해 인가 기능을 구현했다.

 

필터와 인터셉터

웹에서 공통 관심사항을 해결하기 위한 기능들.

AOP도 가능하지만 더 편리한 기능들을 제공함.

 

필터

  • 서블릿이 제공
  • 적용할 url 범위 지정 가능
  • 제한 기능 가능 (적절하지 않은 요청이라고 판단되면 서블릿, 컨트롤러 호출 안함)
  • 필터 체인 (여러개의 필터 연속해서 적용) 구성 가능
  • 필터 인터페이스를 구현하고 등록하면 스프링이 싱글톤으로 관리해줌

인터셉터

  •  스프링 mvc에서 제공
  • 컨트롤러 호출 직전에 호출됨, 서블릿 url과 다르게 매우 정밀하게 제어 가능
  • 제한 기능 가능 (적절하지 않은 요청이라고 판단되면 컨트롤러 호출 하지 않을 수 있음)
  • 체인 기능도 제공
  • 핸들러와 모델,뷰 정보까지 받을 수 있음

 

호출 순서

Http request → WAS → 필터 → 디스패처 서블릿 → 스프링 인터셉터 → 컨트롤러

 

인가 구현 - 필터

@Slf4j
public class LoginCheckFilter implements Filter{
    
    private static final String[] whiteList = {"/", "/admin/users/create-form", "/admin/login", "/css/*"};
    
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException{
  
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        
        try{
            //사용자 인증 시작
            if(isLoginCheckPath(requestURI)){ //경로 체크
                HttpSession session = httpRequest.getSession(false);
                if (session == null || session.getAttribute(SessionConst.LOGIN_USER) == null){ //세션에 없음
                    log.info("위임 전 사용자 요청");
                    httpResponse.sendRedirect("/admin/login?redirectURL=" + requestURI);
                    return; // 미인증 사용자 요청 -> 다음 적용 x
                }
            }
            chain.doFilter(request, response);
        } catch (Exception e){
            throw e; // 예외 로깅 가능, 근데 톰캣까지 보내줘야함
        } finally {
        }
    }
    
    //화이트 리스트 안의 url이면 체크 안함
    private boolean isLoginCheckPath(String requestURI){
        return !PatternMatchUtils.simpleMatch(whiteList, requestURI);
    }
}
//설정 파일에 스프링 빈으로 필터 등록
    @Bean
    public FilterRegistrationBean logFilter(){
        FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<> ();
        filterRegistrationBean.setFilter(new LogFilter());
        filterRegistrationBean.setOrder(1);
        filterRegistrationBean.addUrlPatterns("/*");
        
        return filterRegistrationBean;
    }

Filter 인터페이스를 구현한 후 Configuration 에 수동으로 FilterRegistrationBean을 등록하면 된다.

구현해야할 메소드는 3가지가 있는데

  1. init - 필터가 시작될 때
  2. doFilter - 필터의 기능
  3. destroy - 필터가 소멸될 때

모든 메소드는 default로 만들어져서 사용할 메소드만 구현하면 된다.

doFilter만 이용해서 인가를 구현했다. 

 

whiteList (로그인되지 않아도 접근 허용할 url)를 만들어 둔 후

HttpServletRequest에서 session 값을 받아와서 회원정보가 확인되면 doFilter(다음 필터, 컨트롤러 호출)를 호출,

확인되지 않을 시 Response에 redirect를 지정한 뒤 바로 doFilter를 호출하지 않고 끝내 컨트롤러를 호출하지 않도록 했다.

 

인가 구현 - 인터셉터

@Slf4j
public class LoginCheckInterceptor implements HandlerInterceptor{
    
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception{
        
        String requestURI = request.getRequestURI();
            
        HttpSession session = request.getSession(false);
        
        if (session == null || session.getAttribute(SessionConst.LOGIN_USER) == null){ //세션에 없음
            log.info("위임 전 사용자 요청");
            response.sendRedirect("/admin/login?redirectURL=" + requestURI);
            return false;
        }
        return true;
    }
}
@Configuration
public class AppConfig implements WebMvcConfigurer{
    @Override
    public void addInterceptors(InterceptorRegistry registry){
        registry.addInterceptor(new LoginCheckInterceptor())
            .order(1)
            .addPathPatterns("/**")
            .excludePathPatterns("/", "/admin/users/create-users", "/admin/login", "/css/**", "/*.ico","/error");
    }
	...

인터셉터 인터페이스를 구현한 후 Configuration에서 addInterceptors를 구현하면 된다.

인터셉터가 구현해야할 메소드도 3가지가 있다.

  1. preHandle - 컨트롤러 호출 전
  2. postHandle - 호출 후 ( 에러시 호출 안됨)
  3. afterCompletion - 요청 완료 후 - 뷰 호출할때 (에러 여부와 상관없이 항상 호출)

인터셉터 역시 모든 메소드가 default로 만들어져서 사용할 메소드만 구현하면 된다.

preHandle만 이용해서 인가를 구현했다. 

 

HttpServletRequest에서 session 값을 받아와서 회원정보가 확인되면 true를 

확인되지 않을 시 Response에 redirect를 지정한 뒤 false를 호출했다.

스프링 인터셉터는 preHandle에서 false를 반환하면 컨트롤러를 호출하지 않고 끝내버린다.

 

화이트리스트 기능은 설정 파일에서 addPathPatterns, excludePathPatterns를 이용해 필터보다 상세하게 구현이 가능하다.

 

필터 vs 스프링 인터셉터

확실히 구현 코드만 봐도 특별한 이유가 없다면 스프링 인터셉터를 사용해야할 것 같다.

  • 세밀하고 상세한 url 경로 설정 가능 및 경로 설정을 설정파일에서 따로 함(관심사의 분리)
  • 구현의 편리함 (HttpServletRequest를 변환할 필요 없음)
  • 더 많은 확장 기능 구현 ( 핸들러와 모델,뷰 정보까지 받을 수 있음 )

만약 컨트롤러에서 진행되는 로직이 아닌 스프링과 무관하게 전역적으로 처리되는 로직이라면

필터로직이 인터셉터보다 먼저 진행되므로 필터를 사용하는 것도 괜찮은 것 같다.

 

또 인터셉터 대신 AOP를 사용하면 되지 않나 싶지만

  • 컨트롤러는 타입, 실행 메소드, 파라미터, 리턴값이 일정하지 않다.
  • AOP에선 HttpServletRequest를 받아오기 힘들다.

라는 이유때문에 컨트롤러의 호출 과정에 적용되는 경우는 인터셉터가 더 나은 것 같다.

 

번외 ) 세션에서 회원 정보 받아오는 ArgumentResolver 어노테이션으로 구현하기

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Login{
}

@Slf4j
public class LoginUserArgumentResolver implements HandlerMethodArgumentResolver{
    
    @Override
    public boolean supportsParameter(MethodParameter parameter){
        log.info("suppoersParameter");
        
        boolean hasLogin = parameter.hasParameterAnnotation(Login.class); //로그인 어노테이션이 파라미터에 있는가
        boolean hasUserType = User.class.isAssignableFrom(parameter.getParameterType()); //유저를 파라미터로 받는가
        
        return hasLogin && hasUserType;
    }
    
    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        
        log.info("resolveArgument");
        
        HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
        
        HttpSession session = request.getSession(false);
        if (session == null){
            return null;
        }
        return session.getAttribute(SessionConst.LOGIN_USER);
        
    }
}
@Configuration
public class AppConfig implements WebMvcConfigurer{  
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(new LoginUserArgumentResolver());
    }
  1. Argument Resolver 활용
    1. @Login라는 어노테이션 직접 만들기
    2. Login 이라는 어노테이션 만들어서 로그인 됐는지 확인
    3. argumentResolver를 직접 만들어서 등록함
    4. Target : 사용 범위
    5. Retention : 어노테이션 정보를 남길 기간 - 거의 런타임만 씀

+ Recent posts