본문으로 바로가기

728x90

OAuth 기반으로 Request의 인증처리를 하기 위해서 전역으로 보안검사를 하는 구조를 만들기 위한 가장 간단한 방법은 Spring Framework의 Interceptor또는 Filter를 사용해야 합니다.

 

아래의 그림은 전체적인 Spring에서 Request를 처리하는 과정입니다. 그림에서 Filter는 Spring의 DispatcherServlet 외부에서 Interceptor는 DispatcherServlet의 내부에서 동작하는 것을 볼 수 있습니다.

Spring MVC request life cycle

Interceptor vs Filter

위 그림의 구성도를 참고해 보면 Filter에서는 모든 요청에 대해서 전처리 또는 후처리를 처러할 수 있으며 Interceptor에서는 요청에 대하여 보다 상세적인 처리가 가능합니다. 즉, Interceptor에서는 Controller에서 설정된 값들이나 결과값들을 참고하여 추가적인 처리가 생성가능하나 Filter에서는 그렇게 할 수 없습니다. (물론, Response의 결과물에 특정코드나 값들을 삽입한다면 불가능하지는 않을 것으로 보입니다.)

 

Interceptor

  • DispatcherServlet 이후에 실행된다.
  • @RequestMapping 선언으로 정해진 컨트롤러가 있다면 HandlerMethod로 추가적인 정보를 얻을 수 있다.
  • Handler를 호출하기 전/후, View처리 완료 시점에 각각의 추가적인 처리가 가능하다.
  • HandlerMethod에 정의된 값을 참고하여 인증처리를 할 수 있습니다. 즉, 접근하는 항목은 접근범위(OAuth의 Role또는 Scope 등)의 구체적인 구현이 가능해 집니다.
public interface HandlerInterceptor {
  boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler);
  void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView mav);
  void afterCompletion(HttpServletRequest request, HttpServeletResponse response, Object handler, Exception ex);
}

Filter

  • DispatcherServlet 이전에 실행된다.
  • Request 및 Response 내용을 변경할 수 있다.
  • 보안상 공통적으로 요구되는 암호화/복호화처리가 가능합니다. 즉, Request에서는 전달받은 값을 복호화(E2E데이터 암호의 자동복호화 등)처리하고 Respose에서는 값을 암호화(응답값 난독화 등)처리합니다.
public interface Filter {
  void doFilter(ServletRequest request, ServletResponse response, FilterChain chain);
}

 

Interceptor 사용하기

Interceptor 구현하기

아래의 예시 코드는 Controller 매핑 후에 @PreOAuthAuthorize 어노테이션을 기준으로 접근 범위를 확인하여 응답 또는 오류를 처리하는 로직 예입니다. 헤더의 OAuth Token에 정의된 범위와 어노테이션에 정의된 값을 비교하여 처리합니다.

package com.oauth.filter;

import java.io.IOException;
import java.util.List;
import java.util.Set;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import com.oauth.annotation.PreOAuthAuthorize;
import com.oauth.token.CipherKeypair;
import com.oauth.token.TokenConverter;
import com.oauth.token.TokenManager;
import com.oauth.util.OAuthUtil;
import com.oauth.util.StringUtil;

import io.jsonwebtoken.Claims;

public class OAuthInterceptor extends HandlerInterceptorAdapter {
	private static final Logger logger = LoggerFactory.getLogger(OAuthInterceptor.class);

	@Autowired
	private TokenManager tokenManager;
	
	@Autowired
	@Qualifier("cipherKeypair")
	private CipherKeypair cipherKeypair;
	
	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
		logger.info("===== before(interceptor) =====");
		logger.debug("Handler : {}", handler );

//		java.lang.ClassCastException: org.springframework.web.servlet.resource.ResourceHttpRequestHandler cannot be cast to org.springframework.web.method.HandlerMethod
//		if(handler instanceof ResourceHttpRequestHandler) {
//			ResourceHttpRequestHandler reqhandler = (ResourceHttpRequestHandler) handler;
//			reqhandler.getMethodAnnotation(PreOAuthAuthorize.class);
//		}
		if(!(handler instanceof HandlerMethod)) return true;
		
		HandlerMethod method = (HandlerMethod) handler;
		
		
		PreOAuthAuthorize preAuthorize = method.getMethodAnnotation(PreOAuthAuthorize.class);
		if(preAuthorize == null) return true;

		boolean authorize = preAuthorize.authorize();
		
		// 비인가상태에서도 접근할 수 있는지 여부 확인(인증을 사용하지 않는 경우)
		if(authorize == false) return true;
		
		// 허용 범위 확인
		String scope = preAuthorize.scope();
		String role = preAuthorize.role();
		logger.debug("PreAuthorize value : {} : {}", scope, role);
		if(scope.equals("*") && role.equals("*")) return true;
		

		String headerToken = request.getHeader("Authorization");
		Claims tokenBody = tokenManager.parseToken(headerToken);
		if(tokenBody == null) {
			response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Token is not availiable.");
			return false;
		}

		List<String> tokenScope1 = (List<String>) tokenBody.get("scope");
		Set<String> tokenScope = OAuthUtil.toSet(tokenScope1);
		Set<String> authorizeScope = OAuthUtil.toSet(scope);
		

		List<String> tokenAuthority1 = (List<String>) tokenBody.get("authority");
		Set<String> tokenAuthority = OAuthUtil.toSet(tokenAuthority1);
		Set<String> authorizeAuthority = OAuthUtil.toSet(role);
		
		
		if(!OAuthUtil.hasSet(authorizeScope, tokenScope) || !OAuthUtil.hasSet(authorizeAuthority, tokenAuthority)) {
			response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "You do not have permission to the resource.");
			return false;
		}
		
		return true;
	}

	@Override
	public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
		logger.info("===== after(interceptor) =====");
	}

	@Override
	public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
			throws Exception {
		logger.info("===== afterCompletion =====");
	}
}

Interceptor 등록하기

WebMvcConfigurer의 구현체에 아래와 같이 Interceptor를 등록합니다. @Bean을 사용해야 Interceptor에서 @Autowired 어노테이션의 정상적인 사용이 가능합니다.

package com.oauth.config;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import com.oauth.filter.OAuthInterceptor;

@Configuration
public class OAuthInterceptorConfig implements WebMvcConfigurer {
	private static final Logger logger = LoggerFactory.getLogger(OAuthInterceptorConfig.class);

//	아래와 같이 등록하면 Interceptor에서 @Authwired를 사용할 수 없습니다.(값이 null로 매핑됨)
//	@Override
//	public void addInterceptors(InterceptorRegistry registry) {
//		logger.debug("add Interceptor...................");
//		
//		registry.addInterceptor(new OAuthInterceptor())
//			.addPathPatterns("/**")
//			.excludePathPatterns("/oauth/**"); // OAuth 요청은 예외처리를 한다.
//	}
	
	@Bean
	public OAuthInterceptor makeOAuthInterceptor() {
		return new OAuthInterceptor();
	}

	public @Override void addInterceptors(InterceptorRegistry registry) {
		registry.addInterceptor(makeOAuthInterceptor())
			.addPathPatterns("/**")
			.excludePathPatterns("/oauth/**"); // OAuth 요청은 예외처리를 한다.;
	}
	
}

Interceptor 처리하기

@RestController
public class MenuController extends BaseController{
	private static final Logger logger = LoggerFactory.getLogger(MenuController.class);
	
    /**
	 * 모든 메뉴 목록 출력
	 * @return
	 */
	@PreOAuthAuthorize(scope="read", role="user,manager,developer,administrator")
	@RequestMapping(value="/json/menu.list.ijc", method=RequestMethod.POST)
	public Map<String, Object> menuList() {
		logger.debug("menuList");
		......
	}
}
        

 

Filter 사용하기

Filter 구현하기

아래의 예시는 Request Header의 Authorization 를 얻어와서 복호화를 한 후 다시 Authorization 헤더를 설정하는 코드입니다.

package com.oauth.filter;

import java.io.IOException;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class OAuthFilter implements Filter {
	private static final Logger logger = LoggerFactory.getLogger(OAuthFilter.class);

	@Override
	public void init(FilterConfig filterConfig) throws ServletException {
		logger.debug("Init filter!!");
	}

	@Override
	public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
		logger.debug("Do filter");
		
		if (servletRequest instanceof HttpServletRequest && servletResponse instanceof HttpServletResponse) {
//			String url = ((HttpServletRequest) servletRequest).getRequestURI().toString();
//			String queryString = ((HttpServletRequest) servletRequest).getQueryString();
//			logger.debug("url::" + url);
//			logger.debug("url::" + queryString);
        
			HttpServletRequest request = (HttpServletRequest) servletRequest;
			HttpServletResponse response = (HttpServletResponse) servletResponse;
			String authorizationHeader = request.getHeader("Authorization");
			if(StringUtil.isBlank(authorizationHeader)) {
				response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Authorization Header is required.");
			}
			logger.debug("Authorization Header : {}", authorizationHeader);
			
			String authorizationKeyHeader = request.getHeader("AuthorizationKey");
			if(StringUtil.isBlank(authorizationKeyHeader)) {
				response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Authorization Key Header is required.");
			}
			logger.debug("Authorization Key Header : {}", authorizationKeyHeader);
	
			
			String[] headers = OAuthUtil.parseAuthorizationHeader(authorizationHeader);
			if(headers == null || headers.length != 1) {
				response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Authorization Header is invalidated.");
			}
			
			
			String headerToken = headers[0];
			TokenConverter converter = new TokenConverter(cipherKeypair.getPrivateKey(), authorizationKeyHeader);
			String tokenString = converter.decrypt(headerToken);
			if(!StringUtil.isBlank(tokenString)) {
				MutableHttpServletRequest mutableRequest = new MutableHttpServletRequest(request);
				mutableRequest.putHeader("Authorization", "Bearer" + headerToken);
				filterChain.doFilter(mutableRequest, servletResponse);
				return;
			}
		}
		filterChain.doFilter(servletRequest, servletResponse);
	}

	@Override
	public void destroy() {
		logger.debug("destroy filter!");
	}
}

final class MutableHttpServletRequest extends HttpServletRequestWrapper {
	// holds custom header and value mapping
	private final Map<String, String> customHeaders;

	public MutableHttpServletRequest(HttpServletRequest request) {
		super(request);
		this.customHeaders = new HashMap<String, String>();
	}

	public void putHeader(String name, String value) {
		this.customHeaders.put(name, value);
	}

	public String getHeader(String name) {
		// check the custom headers first
		String headerValue = customHeaders.get(name);

		if (headerValue != null) {
			return headerValue;
		}
		// else return from into the original wrapped object
		return ((HttpServletRequest) getRequest()).getHeader(name);
	}

	public Enumeration<String> getHeaderNames() {
		// create a set of the custom header names
		Set<String> set = new HashSet<String>(customHeaders.keySet());

		// now add the headers from the wrapped request object
		@SuppressWarnings("unchecked")
		Enumeration<String> e = ((HttpServletRequest) getRequest()).getHeaderNames();
		while (e.hasMoreElements()) {
			// add the names of the request headers into the list
			String n = e.nextElement();
			set.add(n);
		}

		// create an enumeration from the set and return
		return Collections.enumeration(set);
	}
}

Filter 등록하기

package com.oauth.config;

import java.util.Arrays;

import org.springframework.boot.autoconfigure.web.servlet.WebMvcRegistrations;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import com.oauth.filter.OAuthFilter;

@Configuration
public class OAuthFilterConfig implements WebMvcConfigurer, WebMvcRegistrations {
	@Bean
	public FilterRegistrationBean<OAuthFilter> myFilter() {
		FilterRegistrationBean<OAuthFilter> bean = new FilterRegistrationBean<OAuthFilter>();
		bean.setFilter(new OAuthFilter());
		bean.setUrlPatterns(Arrays.asList("/*")); // 요청으로 오는 애들만 필러링
		return bean;
	}
}

 

글을 마치며

제가 수행하고 있는 실제 프로젝트에서는 Filter에서 복호화를 처리하지 않고 Interceptor에서 복호화를 처리하리 하였습니다. 위 예제는 Filter와 Interceptor의 차이점을 설명하기 위하여 분리하고 예시코드를 작성한 것입니다.

 

 

참고 자료

 

728x90