Redis를 활용한 DDoS 방어 기능 모듈화

최정환·2024년 7월 7일
0
post-thumbnail

최근 프로젝트에서 DDoS 공격 방어를 위한 개선 작업을 진행했습니다.

🔍 직면한 문제

기존 시스템에서는 Spring Session Data Redis를 사용하여 세션을 Redis에서 관리했습니다.

하지만 동시 요청이 발생할 때 세션이 요청을 처리하기 전에 병렬로 요청이 처리되어 카운트 기능이 제대로 작동하지 않았습니다.

이는 다음 요청에서야 카운트가 적용되어 DDoS 공격을 효과적으로 방어하지 못하는 문제를 일으켰습니다.

spring-session-data-redis를 사용한 세션 서버 블로그는 잘 정리되어 있는 포스트를 가져왔습니다.

Redis를 사용하는 session with spring

HTTP에서 세션 사용 문제점

  • 세션 관리의 비효율성: HTTP 세션을 통해 쿠키를 사용하여 요청을 관리하면, 동시 요청이 많은 상황에서 세션 관리의 부하가 증가합니다.

  • 서블릿의 동시성 처리: 서블릿은 동시 요청을 처리할 때, 여러 요청을 병렬로 처리하지만, 요청 간의 동기화 문제가 발생할 수 있습니다.

문제 설명

동시 다발적으로 요청이 들어올 때, 세션을 통해 Redis에서 카운트를 관리하는 방식은 세션에 카운트를 저장하기 전에 모든 요청이 처리되어 버립니다.

이로 인해 카운트가 쌓이는 기능이 지연되어 제대로 동작하지 않습니다.


일반적인 세션 관리

현재의 문제점

🔧 개선된 해결책

이 문제를 해결하기 위해 메시징 기능 전체를 컴포넌트로 만든 후 RedisTemplate을 사용해 명시적으로 요청을 처리하고 카운트를 관리하도록 개선했습니다.

이를 통해 성능을 향상시키고 모듈화하여 재사용성을 높였습니다.

또한, Redis는 비즈니스 로직의 코드를 줄여주는데도 기여했습니다. 기존에는 세션에 최초 요청의 시각 값을 담아 다음 요청의 시각과 비교하여 5분 후면 카운트를 초기화하는 로직도 함께 코드상에 존재했지만, expire를 사용하며 더 간결하고 안전한 코드가 되었습니다.

🛠️ 개선된 코드 예시

@Component
public class RequestLimiter {

    @Autowired
    private RedisTemplate<String, Integer> redisTemplate;

    private static final int MAX_REQUESTS = 10;
    private static final long EXPIRE_DURATION = 1L; // duration in minutes

    public void synchronized protectRateLimit(String redisKey) {
        // 현재 요청 수를 먼저 조회
        Integer currentCount = redisTemplate.opsForValue().get(redisKey);

        // 이미 최대 요청 수를 초과했는지 확인
        if (currentCount != null && currentCount >= MAX_REQUESTS) {
            throw new RuntimeException("MAX_REQUESTS 초과");
        }

        // 요청 카운트를 증가시키고, 처음이라면 만료 시간 설정
        Long incremented = redisTemplate.opsForValue().increment(redisKey);
        
        if (incremented != null && incremented == 1) {
            redisTemplate.expire(redisKey, EXPIRE_DURATION, TimeUnit.MINUTES);
        }
    }

    public boolean isRequestAllowed(String sessionId) {
        try {
            protectRateLimit(sessionId);
            return true;
        } catch (RuntimeException e) {
            return false;
        }
    }
}

synchronized 괜찮을까?

  • 목표가 단순 카운팅: 요청을 카운트하고 특정 횟수를 넘으면 막기 위한 로직.
  • 카운팅은 증가 연산: INCR 같은 Redis의 명령어는 원자적(atomic)으로 동작
  • 최대 요청 수 초과만 확인: 이미 MAX_REQUESTS를 넘었다면 추가 요청을 막을 수 있음.

✅ 결과

RedisTemplate을 사용한 개선 후, 요청을 더 빠르게 처리할 수 있었고 동시 요청 시에도 정확한 카운팅이 가능해졌습니다. 이를 통해 DDoS 공격을 효과적으로 방어할 수 있었습니다.

🏁 결론

Spring Session Data Redis를 사용한 방식은 성능상의 한계로 인해 DDoS 방어에 적합하지 않을 수 있습니다.

RedisTemplate을 사용한 명시적 캐싱을 통해 성능을 개선하고 DDoS 공격을 효과적으로 방어할 수 있습니다.
이를 통해 시스템의 안정성과 보안을 강화할 수 있었습니다.

0개의 댓글