본문 바로가기
팀 프로젝트/cheerha.project

IP와 Email 차단을 DB로 옮기고 IP 전역 밴 추가하기

by pon9 2025. 2. 20.

IP와 Email차단 DB로 옮기기

차단된 이메일, 아이피의 ttl이 너무 길어서 redis를 불필요하게 사용중이다.

그래서 차단된 유저의 정보는 데이터베이스로 옮겼당

 

 

IP 글로벌밴 기능 추가하기

IP밴된 사용자를 모든 api에 접근하지 못하게 하려고 인터셉터로 구현했다.

public class IpBlockingInterceptor implements HandlerInterceptor {

    private final BannedIpRepository bannedIpRepository;

    @Override
    public boolean preHandle(파라미터) {
        String ip = getClientIp(request);

        if (bannedIpRepository.existsByIp(ip)) {
            log.warn("차단된 IP 접근 시도: {}", ip);
            throw new UnAuthorizedException(AuthErrorCode.BANNED_IP);
        }
        return true;
    }
}

@Configuration
public class WebConfig implements WebMvcConfigurer {

    private final IpBlockingInterceptor ipBlockingInterceptor;

    //모든 요청에 ip 밴 적용
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(ipBlockingInterceptor)
                .addPathPatterns("/**");
    }
}

하지만 인터셉터로 구현했더니 한가지 문제점이 있었다.

jwt토큰관련 필터가 걸린 api에서는 토큰 exception이 먼저 출력됐고, 이는 jwtfilter가 인터셉터보다 앞단에 있음을 의미했다.

이처럼 필터가 웹mvc에 존재하며, 스프링mvc에 존재하는 인터셉터보다 전방에 위치해 생긴 문제로 IP blocking에는 필터체인을 사용해야 했다.

웹mvc는 스프링과 무관하게 전역적으로 처리해야 하는 작업들을 모아놓은 곳이며 대표적으로 보안 작업, spring security가 있다.

스프링mvc는 클라이언트의 요청과 관련되어 전역적으로 처리해야 하는 작업들이 있다.

 

이렇게 해서 생기는 대표적인 문제는, jwtfilter에서 처리되는 servletException은 스프링에 의한 예외처리(globalexceptionHandler)를 적용받을 수 없고, 예외처리를 직접 해줘야한다.

어쩐지 jwtFilter에서 예외처리 관련 리팩토링을 할 때 '왜 서블릿 익셉션은 전역적으로 관리되지 않는거지?' 하면서 코드를 한 곳에 모아놨는데

이제야 이유를 알게됐다 ㅎ; (지금에라도 알아서 다행)

public class JwtFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
        throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        //필터 관련 로직(생략)
        try {
        	//필터 관련 로직(생략)
        } catch (ExpiredJwtException e) {
            log.error("Expired JWT token, 만료된 JWT token 입니다.", e);
            sendErrorResponse(httpResponse, HttpStatus.UNAUTHORIZED, "만료된 JWT 토큰입니다.");
        } catch (SecurityException | MalformedJwtException e) {
            log.error("Invalid JWT signature, 유효하지 않은 JWT 서명 입니다.", e);
            sendErrorResponse(httpResponse, HttpStatus.UNAUTHORIZED, "유효하지 않은 JWT 서명입니다.");
        } catch (UnsupportedJwtException e) {
            log.error("Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다.", e);
            sendErrorResponse(httpResponse, HttpStatus.BAD_REQUEST, "지원되지 않는 JWT 토큰입니다.");
        } catch (IllegalArgumentException e) {
            log.error("Invalid JWT token, 유효하지 않은 JWT 토큰 입니다.", e);
            sendErrorResponse(httpResponse, HttpStatus.BAD_REQUEST, "잘못된 JWT 토큰 형식입니다.");
        } catch (Exception e) {
            log.error("예상치 못한 예외 발생", e);
            sendErrorResponse(httpResponse, HttpStatus.INTERNAL_SERVER_ERROR, "서버 오류가 발생했습니다.");
        }
    }
    private void sendErrorResponse(HttpServletResponse response, HttpStatus status, String message) throws IOException {
        response.setStatus(status.value());
        response.setContentType("application/json");
        response.setCharacterEncoding("UTF-8");

        Map<String, Object> errorResponse = new HashMap<>();
        errorResponse.put("status", status.value());
        errorResponse.put("error", status.getReasonPhrase());
        errorResponse.put("message", message);

        response.getWriter().write(new ObjectMapper().writeValueAsString(errorResponse));
    }
}

 

하단의 sendErrorResponse를 원래 globalExceptionHandler에 만들었다가,

계속 500 서블릿에러만 일관되게 나와서

아 서블릿에러는 먼저 처리되나보다 지레 짐작하며 jwtfilter에 가져다 놨었다.

어렴풋이 알고 있던 것들이 조립되며 뭔가 깨달음을 얻어서 spring security도 할 수 있을 것 같은 자신감이 생겼지만 일단 접어두자

 

어쨋든 다시 돌아와서, interceptor로 구현한 걸 filter로 재구성하고 필터체인에 걸어주자

@Order(Ordered.HIGHEST_PRECEDENCE)  //필터 우선순위 최상위
public class IpBlockingFilter implements Filter {

    private final BannedIpRepository bannedIpRepository;

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {

        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        String ip = getClientIp(httpRequest);

        if (bannedIpRepository.existsByIp(ip)) {
            log.warn("차단된 IP 접근 시도: {}", ip);
            httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN, "차단된 IP입니다.");
            return;
        }
        chain.doFilter(request, response);
    }
}

@Configuration
public class FilterConfig {

    @Bean
    public FilterRegistrationBean<Filter> ipBlockingFilterRegistration(IpBlockingFilter ipBlockingFilter) {
        FilterRegistrationBean<Filter> registrationBean = new FilterRegistrationBean<>();
        registrationBean.setFilter(ipBlockingFilter);
        registrationBean.addUrlPatterns("/*");
        registrationBean.setOrder(1);
        return registrationBean;
    }

    @Bean
    public FilterRegistrationBean<JwtFilter> jwtFilter() {
        FilterRegistrationBean<JwtFilter> registrationBean = new FilterRegistrationBean<>();
        registrationBean.setFilter(new JwtFilter(jwtSecurityProperties, redisBlackListService, jwtUtil));
        registrationBean.addUrlPatterns("/*");
        registrationBean.setOrder(2);

        return registrationBean;
    }
}

 

ipBlockingFilter는 스프링에서 적용해주는 필터가 아니기 때문에 

setOrder()에 들어가는 숫자가 작을수록 우선순위에 적용된다.

이제 토큰을 사용하는 api를 호출해도 ip밴 exception이 먼저 적용된다

 

여전히 남은 문제점:

공용ip 어떻게 처리하지?

Ip, Email 밴을 풀어주는 기능이 있어야겠는데 관리자 페이지를 만들까?