반응형
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
}
}
반응형
'IT > JAVA' 카테고리의 다른 글
Java Garbage Collection (1) | 2020.11.09 |
---|---|
Java HashMap은 어떻게 동작하는가? (0) | 2020.11.05 |
Spring Boot Resource 사용시 접두사(classpath, file 등)를 사용해야 하는 이유 (2) | 2019.10.25 |
@Autowired 필드주입 Spring 없이 mock 생성하여 테스트하기 (0) | 2019.10.17 |
스택 계산기-(후위연산식을 통해 계산기를 만들어 보자!) (0) | 2019.10.10 |