[Spring] GeoIp2를 사용한 해외 아이피 차단

이멀젼씨

·

2021. 7. 28. 09:57

목적

해외에서 들어오는 해킹시도를 막기 위함

목차

  1. 해킹시도
  2. 해외 IP 차단하기

1. 해킹시도

최근에 제작한 서비스를 AWS EC2에 배포하여 운영중에 다음과 같은 예외를 보게 되었다.

처음보는 IP에서 수상한 url 경로로 접속한 것이었다.

찾아보니

/index.php s=/index/ think app/invokefunction&function ...

러시아의 모스크바에서 php서버 취약점에 대한 공격시도였다.

다행히 내 서비스는 스프링부트여서 예외를 뱉어냈다.

하지만 그럼에도 내 서비스에는 해외에서 접근할 일이 없기에 해외 IP의 접근을 모두 막고 싶었다.

또한 스프링부트에서 해외 IP차단에 관련한 글은 찾기가 힘들어 이에 대한 가이드 라인을 제공하고자 글을 작성하게 되었다.

2. 해외 IP 차단하기

Maxmind에서 제공하는 DB를 사용하여 어느 나라에서 접근한지 파악한 뒤 한국에서 접근한 경우에만 접근을 허용할 예정이다.

1. Maxmind 가입

https://www.maxmind.com/en/home
IP를 나라에 매칭시키는 DB를 제공받기 위해선 위의 서비스에 가입해야 한다.

가입 시 비밀번호 입력 창이 없어서 당황했는데, 가입 후에 Forgot your password?를 클릭하여 가입 시 입력한 이메일을 넣는다.

그럼 가입한 이메일로 비밀번호 설정 링크가 오고 해당 링크를 클릭하여 비밀번호를 재설정 한다.

다시 홈페이지로 가서 username엔 가입 시 입력했던 이메일을, password엔 재설정한 비밀번호를 입력한 뒤 로그인한다.

2. Download Files로 이동


로그인 후 위와 같은 화면으로 이동되지 않은 경우, 사람모양을 누른 뒤 My Account를 누르면 동일한 화면으로 이동된다.


스크롤을 아래로 내려 Download Files를 클릭한다.

3. DB 다운로드


스크롤을 내려서 GeoLite2 CountryDownload Gzip을 눌러 다운로드 받고 압축을 해제한다.

4. GeoLite2-Country.mmdb 위치시키기

GeoLite2-Country.mmdb를 스프링 resource에 위치시키거나 프로젝트 외부에 위치시킬 수 있다.

내부에 위치시키는 경우에는 Maxmind에서 제공하는 DB가 업데이트 될 때마다 새로 빌드를 하여 배포시켜야 한다는 단점이 있다.

큰 수준의 업데이트는 아니겠지만 필자는 그래도 DB를 프로젝트 외부로 빼두었다.

5. 라이브러리 추가하기

implementation group: 'com.maxmind.geoip2', name: 'geoip2', version: '2.15.0'

GeoLite2-Country.mmdb를 읽기 위해선 위의 라이브러리가 필요하다.

6. GeoIp2Config 설정

geoip2:
  database:
    path: /home/ubuntu/GeoLite2-Country.mmdb
@Configuration
@Profile("prod")
public class GeoIp2Config {

    @Value("${geoip2.database.path}")
    private String path;

    @Bean
    public DatabaseReader databaseReader() throws IOException, GeoIp2Exception{
        File resource = new File(path);
        return new DatabaseReader.Builder(resource).build();
    }
}

필자는 외부에 두고 test때에는 사용되지 않도록 설정하기 위해 @Profile("prod")를 추가하였다.

DB를 resource/GeoLite2-Country.mmdb 경로에 둔 경우엔 파일 경로를 아래와 같이 입력해주면 된다.

classpath:/GeoLite2-Country.mmdb

7. IPAuthenticationFilter 생성

public class HttpReqResUtils {  

    private static final String[] IP_HEADER_CANDIDATES = {  
            "X-Forwarded-For",  
  "Proxy-Client-IP",  
  "WL-Proxy-Client-IP",  
  "HTTP_X_FORWARDED_FOR",  
  "HTTP_X_FORWARDED",  
  "HTTP_X_CLUSTER_CLIENT_IP",  
  "HTTP_CLIENT_IP",  
  "HTTP_FORWARDED_FOR",  
  "HTTP_FORWARDED",  
  "HTTP_VIA",  
  "REMOTE_ADDR"  
  };  

 public static String getClientIpAddressIfServletRequestExist() {  

        if (RequestContextHolder.getRequestAttributes() == null) {  
            return "0.0.0.0";  
  }  

        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();  
 for (String header: IP_HEADER_CANDIDATES) {  
            String ipList = request.getHeader(header);  
 if (ipList != null && ipList.length() != 0 && !"unknown".equalsIgnoreCase(ipList)) {  
                String ip = ipList.split(",")[0];  
 return ip;  
  }  
        }  

        return request.getRemoteAddr();  
  }  
}
@Component
@Profile("prod")
@Slf4j
public class IpAuthenticationFilter implements Filter {

    @Autowired
    private DatabaseReader databaseReader;

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        String ipAddress = HttpReqResUtils.getClientIpAddressIfServletRequestExist();
        InetAddress inetAddress = InetAddress.getByName(ipAddress);
        String country = null;
        try {
            country = databaseReader.country(inetAddress).getCountry().getName();
        } catch (GeoIp2Exception e) {
            e.printStackTrace();
        }
        if(country == null || !country.equals("South Korea")){
            log.info("Access Rejected : {}, {}", ipAddress, country);
            return;
        }
        chain.doFilter(request, response);
    }

    @Override
    public void init(FilterConfig filterConfig) {
        log.info("IP Authentication Filter Init..");
    }

    @Override
    public void destroy() {
        log.info("IP Authentication Filter Destroy..");
    }
}

마찬가지로 test시엔 만들지 않을 것이기 때문에 @Profile("prod")를 설정해 주었다.

Filter인터페이스의 init과 destroy메소드는 별 다른 내용없이 구현해주었다.

doFilter메소드는 HttpRequestUtil&을 사용하여 유효한 IP를 추출한 뒤 InetAddress로 바꾸어 어느 위치인지 조회한다.

조회한 결과를 문자열 country에 할당하는데 할당이 안되어있거나, 할당된 결과가 South Korea가 아닌 경우엔 더 이상 다음 필터를 진행하지 못하도록 바로 return 시킨다.

8. 필터 등록

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired(required = false)
    private IpAuthenticationFilter ipAuthenticationFilter;

    @Value("${spring.profiles.active:unknown}")
    private String profile;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .httpBasic().disable()
                .cors()
                .and()
                .csrf().disable()
                .formLogin().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .addFilterBefore(new JwtAuthenticationFilter(jwtAuthenticationProvider), UsernamePasswordAuthenticationFilter.class);

        if(profile.equals("prod")) http.addFilterBefore(ipAuthenticationFilter, JwtAuthenticationFilter.class);
    }
}

불필요한 부분은 제거하고 필요한 설정만 가져왔다.

@Authwired(required = false)if(profile.equals("prod")) 를 통해 test시엔 IpAuthenticationFilter를 주입받지도, 필터에 껴넣지도 못하도록 하였다.

현재 프로파일이 prod 인 경우엔 스프링 시큐리티의 제일 앞단에서 동작하도록 필터를 추가해주었다.

스프링만 사용한다면 FilterRegistrationBean 을 사용하여 적절한 위치에 추가하면 될 듯 싶다.

9. IP차단 확인

해외에서 오는 아이피는 모조리 차단되었음을 알 수 있다.