본문 바로가기
Spring

Spring Security Web을 사용하여 IP 접근 제어 구현하기

by Leica 2021. 9. 1.
반응형

Spring security webIpAddressMatcherspring interceptor를 이용해 클라이언트 IP가 특정 IP 대역에 맞는지 체크하는 접근 제어 로직을 구현해보자.

 

Dependency

implementation 'org.springframework.security:spring-security-web'

spring-security-web 의존성을 추가한다.

 

Interceptor 생성 및 등록

@Slf4j
@RequiredArgsConstructor
@Component
public class IpAddressAccessControlInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        // TODO

        return true;
    }
}

HandlerInterceptor를 구현하여 IP 접근 제어용 interceptor를 만든다.

Spring interceptor를 반드시 bean으로 등록해야하는건 아니지만 bean으로 등록하면 필요한 의존성을 편리하게 주입받을 수 있다.

추후에 로직에 필요한 bean을 주입받을 것이기 때문에 미리 @Component를 붙여두었다.

 

@Configuration
@RequiredArgsConstructor
public class WebMvcConfig implements WebMvcConfigurer {

    private final IpAddressAccessControlInterceptor ipAddressAccessControlInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {

        registry.addInterceptor(ipAddressAccessControlInterceptor)
                .order(1)
                .addPathPatterns("/url1", "/url2");
    }
}

Interceptor는 WebMvcConfigurer 구현체에 addInterceptors를 override하여 등록한다.

addPathPatterns()에 접근 제어를 적용할 리소스 url를 설정한다.

 

Client Ip

IP 접근제어를 하려면 우선 클라이언트의 IP 주소를 가져와야한다.

이를 위한 유틸 메소드를 추가한다.

 

public static String getClientIp(HttpServletRequest request) {

    String ip = StringUtils.trimToNull(TextUtils.getSplitValue(request.getHeader("X-Forwarded-For"), ",", 0));

    if (ip == null || ip.length() == 0 || ip.equalsIgnoreCase("unknown")) {
        ip = request.getHeader("x-real-ip");
    }
    if (ip == null || ip.length() == 0 || ip.equalsIgnoreCase("unknown")) {
        ip = request.getHeader("x-original-forwarded-for");
    }
    if (ip == null || ip.length() == 0 || ip.equalsIgnoreCase("unknown")) {
        ip = request.getHeader("Proxy-Client-IP");
    }
    if (ip == null || ip.length() == 0 || ip.equalsIgnoreCase("unknown")) {
        ip = request.getHeader("WL-Proxy-Client-IP");
    }
    if (ip == null || ip.length() == 0 || ip.equalsIgnoreCase("unknown")) {
        ip = request.getHeader("HTTP_X_FORWARDED_FOR");
    }
    if (ip == null || ip.length() == 0 || ip.equalsIgnoreCase("unknown")) {
        ip = request.getHeader("HTTP_X_FORWARDED");
    }
    if (ip == null || ip.length() == 0 || ip.equalsIgnoreCase("unknown")) {
        ip = request.getHeader("HTTP_X_CLUSTER_CLIENT_IP");
    }
    if (ip == null || ip.length() == 0 || ip.equalsIgnoreCase("unknown")) {
        ip = request.getHeader("HTTP_CLIENT_IP");
    }
    if (ip == null || ip.length() == 0 || ip.equalsIgnoreCase("unknown")) {
        ip = request.getHeader("HTTP_FORWARDED_FOR");
    }
    if (ip == null || ip.length() == 0 || ip.equalsIgnoreCase("unknown")) {
        ip = request.getHeader("HTTP_FORWARDED");
    }
    if (ip == null || ip.length() == 0 || ip.equalsIgnoreCase("unknown")) {
        ip = request.getHeader("HTTP_VIA");
    }
    if (ip == null || ip.length() == 0 || ip.equalsIgnoreCase("unknown")) {
        ip = request.getHeader("REMOTE_ADDR");
    }
    if (ip == null || ip.length() == 0 || ip.equalsIgnoreCase("unknown")) {
        ip = request.getRemoteAddr();
    }

    return ip;
}

다양한 종류의 proxy를 고려하여 가능한 header를 전부 확인하는 것이다.

 

이 메소드를 사용해 interceptor에서 클라이언트 IP를 가져올 수 있도록 코드를 추가한다.

 

@Slf4j
@RequiredArgsConstructor
@Component
public class IpAddressAccessControlInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        String clientIpAddress = WebUtils.getClientIp(request);	// 추가

        // TODO

        return true;
    }
}

 

IpAddressMatcher

IpAddressMatcher는 객체 생성 시에 접근을 허용할 IP 또는 IP 대역을 넘기도록 되어있다.

그리고 IP 체크는 matches() 메소드에 체크 대상 IP를 넘겨 결과를 받는다.

// IpAddressMatcher 객체 생성
IpAddressMatcher matcher = new IpAddressMatcher("192.168.1.0/24");

// match 여부 확인
boolean result1 = matcher.matches("192.168.1.14");	// true
boolean result2 = matcher.matches("127.0.0.1");	// false

 

다음과 같이 허용할 IP 대역을 list로 별도의 클래스에서 관리할 수 있도록 manager를 만들었다.

@Component
public class IpAddressMatcherManager {

    @Getter
    private List<IpAddressMatcher> ipAddressMatchers;

    public IpAddressMatcherManager() {

        this.ipAddressMatchers = List.of(
                new IpAddressMatcher("192.168.1.0/24"),
                new IpAddressMatcher("192.168.2.0/24"),
                new IpAddressMatcher("192.168.3.0/24")
        );
    }
}

 

다음으로 이 IpAddressMatcherManager를 사용하는 service를 만든다.

@RequiredArgsConstructor
@Service
public class IpAddressAccessControlService {

    private final IpAddressMatcherManager ipAddressMatcherManager;

    public boolean isAccessible(String ipAddress) {
        List<IpAddressMatcher> ipAddressMatchers = ipAddressMatcherManager.getIpAddressMatchers();
        return ipAddressMatchers.stream().anyMatch(matcher -> matcher.matches(ipAddress));
    }
}

isAccessible()은 파라미터로 받은 IP 주소가 접근 가능한지 여부를 반환하는 메소드이다.

IpAddressMatcherManager를 통해 가져온 matcher list에서 java stream의 anyMatch()를 사용해 간단하게 match 여부를 확인하는 로직을 구현할 수 있다.

 

이제 interceptor에서 이 service를 사용하여 접근 제어 로직을 완성해보자.

코드는 다음과 같다.

@Slf4j
@RequiredArgsConstructor
@Component
public class IpAddressAccessControlInterceptor implements HandlerInterceptor {

    private final IpAddressAccessControlService ipAddressAccessControlService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        String clientIpAddress = WebUtils.getClientIp(request);

        if (!ipAddressAccessControlService.isAccessible(clientIpAddress)) {
            String requestURI = request.getRequestURI();
            log.warn("Forbidden access. request uri={}, client ip={}", requestURI, clientIpAddress);

            response.sendError(HttpServletResponse.SC_FORBIDDEN);
            return false;
        }

        return true;
    }
}

 

이상~!

 

새벽에 잠이 안와 작성해본 포스팅이었습니다.

무서운 불면증..

 

참고

https://stackoverflow.com/questions/577363/how-to-check-if-an-ip-address-is-from-a-particular-network-netmask-in-java

 

 

반응형

댓글