본문 바로가기

IT/JAVA

Filter 를 활용한 ACL 만들기 (feat. Spring)

반응형

1. 개요

  • 업무를 하던중 컨트롤러 레이어에서 허용된 IP 를 체크하는 로직이 상당수 중복코드로 남아 있는것을 발견함
  • IP 체크를 통해 허용된 IP가 아니라면 Exception 을 던지고 있었음
  • 허용되지 않은 IP가 접근하는것을 Exception이 아닌 403(forbidden)응답 또는 은닉화를 위해 404(Not Found) 응답을 돌려줘야 된다고 생각함

2. Before

2.1. Code

2.1.1. Controller


@RequestMapping(value = "/api/test")
public Map<String, Object> apiTest() {
    // 중복코드
    if (!ipInfoService.checkAllowIps()) {
        throw new RuntimeException(
            "[" + appProjectName + " - " + activeProfile + "] 허용되지 않은 Client IP 입니다.");
    }
}

2.2. 고민

  • 중복 로직을 제거하기 위해 AOP, Filter, Interceptor 를 고민했음
  • IP 검사 자체는 맨 앞단에서 발생해야 한다고 생각함
  • Spring 라이브러리에 종속적이지 않아야 한다고 생각함

3. After

3.1. Code

3.1.1. Filter

  • 스프링 종속적인 코드가 없음
  • Filter Config를 구현하여 해당 설정을 통해서 Filter를 등록함

import lombok.extern.slf4j.Slf4j;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;


@Slf4j
public class ApiAllowIpCheckFilter implements Filter {

    private final IpInfoService ipInfoService;

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        log.info("Initializing filter :{}", this);
    }


    public ApiAllowIpCheckFilter(IpInfoService ipInfoService, String[] allowIps) {
        this.ipInfoService = ipInfoService;
        ipInfoService.setAllowIps(allowIps);
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) servletRequest;
        log.info("Starting a transaction for req : {}",  req.getRequestURI());

        if (!ipInfoService.checkAllowIps()) {
            log.info("Forbidden access occurs at that {}", req.getRequestURI());
            HttpServletResponse response = (HttpServletResponse) servletResponse;
            response.sendError(HttpServletResponse.SC_FORBIDDEN);
        }

        filterChain.doFilter(servletRequest, servletResponse);
        log.info("Committing a transaction for req : {}", req.getRequestURI());
    }

    @Override
    public void destroy() {}
}

3.1.2 Filter Config


@Configuration
public class FilterConfig {

    IpInfoService ipInfoService;

    @Value("${app.ip.allow}")
    private String[] allowIps;

    @Autowired
    public FilterConfig(IpInfoService ipInfoService) {
        this.ipInfoService = ipInfoService;
    }

    @Bean
    public ApiAllowIpCheckFilter apiAllowIpCheckFilter(IpInfoService ipInfoService){
        return new apiAllowIpCheckFilter(ipInfoService, allowIps);
    }

    @Bean
    public FilterRegistrationBean ipCheckerFilter(ApiAllowIpCheckFilter apiAllowIpCheckFilter ){

        FilterRegistrationBean registrationBean
                = new FilterRegistrationBean();

        registrationBean.setFilter(apiAllowIpCheckFilter(ipInfoService));
        registrationBean.addUrlPatterns("/api/*");

        return registrationBean;
    }
}

3.2 Test Code

  • testDoFilter : 해당 IP 가 허용 IP일 경우 filterChain 메소드를 무조건 통과함
  • filterChain를 만들고, doFilter 메소드를 오버라이드 하여 filterChain을 만들어 넣어줌
  • 해당 filterChain을 거친다면 원하는 결과값을 가지게 됨
  • ip 에 따라서 정확하게 필터를 통과하는지 확인하는 작업이 목표
  • testForbidden : 해당 IP 가 허용하지 않은 IP일 경우 403(forbidden)이 정확하게 발생하는지 확인
  • servletResponse에 sendError를 오버라이딩해 해당 메소드에서 원하는 기대값을 주입시켜줌
  • 기대값을 검사하는 테스트 실시
  • ip 에 따라서 정확하게 403(forbidden) 발생을 확인하는 작업이 목표

public class ApiAllowIpCheckFilterTest {

    @Mock
    private IpInfoService mockIpInfoService;

    private ApiAllowIpCheckFilter apiAllowIpCheckFilterUnderTest;

    private String[] ips = new String[]{"10.194.10.112"};

    @Before
    public void setUp() throws NoSuchFieldException, IllegalAccessException {
        initMocks(this);
        apiAllowIpCheckFilterUnderTest = new ApiAllowIpCheckFilter(mockIpInfoService, ips);

        Field filed = IpInfoService.class.getDeclaredField("allowIps");
        filed.setAccessible(true);
        filed.set(mockIpInfoService, ips);
    }

    @Test
    public void testInit() throws Exception {
        // Setup
        final FilterConfig filterConfig = mock(FilterConfig.class);

        // Run the test
        apiAllowIpCheckFilterUnderTest.init(filterConfig);

        // Verify the results
    }

    @Test
    public void testDoFilter() throws Exception {

        final AtomicBoolean actual = new AtomicBoolean(false);
        final MockHttpServletRequest servletRequest = new MockHttpServletRequest();
        servletRequest.setRequestURI("/api/test");
        final MockHttpServletResponse servletResponse = new MockHttpServletResponse();
        final FilterChain filterChain = new FilterChain() {
            @Override
            public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse) throws IOException, ServletException {
                actual.set(true);
            }
        };

        final IpInfoService ipInfoService = mock(IpInfoService.class);
        when(ipInfoService.checkApiAllowIps()).thenReturn(true);
        apiAllowIpCheckFilterUnderTest.doFilter(servletRequest, servletResponse, filterChain);
        final boolean actualValue = actual.get();
        final boolean expectedValue = new AtomicBoolean(true).get();

        assertEquals(actualValue, expectedValue);

    }

    @Test
    public void testForbidden() throws IOException, ServletException {

        final AtomicBoolean actual = new AtomicBoolean(false);
        final MockHttpServletRequest servletRequest = new MockHttpServletRequest();
        servletRequest.setRequestURI("/api/test");

        final MockHttpServletResponse servletResponse = new MockHttpServletResponse(){

            @Override
            public void sendError(int status) throws IOException {
                assertEquals(status, HttpStatus.FORBIDDEN.value());
                actual.set(true);
            }
        };

        final IpInfoService ipInfoService = mock(IpInfoService.class);
        when(ipInfoService.checkApiAllowIps()).thenReturn(false);

        FilterChain filterChain = mock(FilterChain.class);
        apiAllowIpCheckFilterUnderTest.doFilter(servletRequest, servletResponse, filterChain);

        final boolean actualValue = actual.get();
        final boolean expectedValue = new AtomicBoolean(true).get();

        assertEquals(actualValue, expectedValue);
    }

    @Test
    public void testDestroy() {
        // Setup

        // Run the test
        apiAllowIpCheckFilterUnderTest.destroy();

        // Verify the results
    }
}
반응형