Protección de APIs Spring Boot contra Abuso y Ataques de Fuerza Bruta

El desarrollo de aplicaicones robustas implica la implementación de medidas de seguridad, especialmente al exponer servicios a Internet. Este tutorial detalla cómo proteger tus endpoints de Spring Boot contra solicitudes maliciosas y ataques de fuerza bruta, utilizando un interceptor y Redis para limitar las peticiones por IP y URL en un período determinado. Si un IP excede el umbral, se bloqueará temporalmente.

La configuración se basa en un proyecto Spring Boot. A continuación, se presenta el código central.

Primero, definimos un interceptor personalizado, que es el componente principle:

import com.fasterxml.jackson.databind.ObjectMapper;
import com.technicalinterest.group.util.ApiResult;
import com.technicalinterest.group.util.IpAdrressUtil;
import com.technicalinterest.group.util.ResultEnum;
import com.technicalinterest.group.util.SpringContextUtil;
import com.technicalinterest.group.util.RedisUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Collections;
import java.util.concurrent.TimeUnit;

/**
 * Interceptor para limitar peticiones repetidas por IP y URL.
 */
@Slf4j
public class RateLimitInterceptor implements HandlerInterceptor {

    private static final String LOCKED_IPS_KEY = "locked_ips:";
    private static final String REQUEST_COUNT_KEY_PREFIX = "request_count:";
    private static final int MAX_REQUESTS = 5; // Límite de peticiones
    private static final int LOCK_DURATION_SECONDS = 60; // Duración del bloqueo en segundos

    private RedisUtil getRedisUtil() {
        // Asumiendo que SpringContextUtil puede obtener beans de Spring
        return SpringContextUtil.getBean(RedisUtil.class);
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String clientIp = IpAdrressUtil.getIpAdrress(request); // Asumiendo que esta utilidad extrae la IP correctamente
        String requestUri = request.getRequestURI();
        log.info("Solicitud recibida - URI: {}, IP: {}", requestUri, clientIp);

        if (isIpLocked(clientIp)) {
            log.warn("IP bloqueada: {}", clientIp);
            sendJsonResponse(response, new ApiResult(ResultEnum.IP_LOCKED)); // Asumiendo que ResultEnum.IP_LOCKED existe
            return false;
        }

        if (!isRequestAllowed(clientIp, requestUri)) {
            log.warn("Límite de peticiones excedido para IP: {}, URI: {}", clientIp, requestUri);
            sendJsonResponse(response, new ApiResult(ResultEnum.TOO_MANY_REQUESTS)); // Asumiendo que ResultEnum.TOO_MANY_REQUESTS existe
            return false;
        }

        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {
        // No se requiere lógica adicional aquí para este caso de uso.
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        // No se requiere lógica adicional aquí para este caso de uso.
    }

    /**
     * Verifica si una dirección IP está temporalmente bloqueada.
     * @param ip La dirección IP a verificar.
     * @return true si la IP está bloqueada, false en caso contrario.
     */
    private boolean isIpLocked(String ip) {
        RedisUtil redisUtil = getRedisUtil();
        return redisUtil.hasKey(LOCKED_IPS_KEY + ip);
    }

    /**
     * Incrementa el contador de peticiones para una IP y URI específicos.
     * Si se supera el límite, bloquea la IP.
     * @param ip La dirección IP del cliente.
     * @param uri La URI de la solicitud.
     * @return true si la petición está permitida, false si se ha superado el límite y la IP ha sido bloqueada.
     */
    private boolean isRequestAllowed(String ip, String uri) {
        RedisUtil redisUtil = getRedisUtil();
        String key = REQUEST_COUNT_KEY_PREFIX + ip + ":" + uri;

        // Usar `opsForValue().increment` y `expire` de Spring Data Redis
        Long count = redisUtil.increment(key, 1L); // Asumiendo que RedisUtil tiene un método `increment` que devuelve el nuevo valor

        if (count == 1) {
            // Primera petición, establecer tiempo de expiración para la clave de contador
            redisUtil.expire(key, LOCK_DURATION_SECONDS, TimeUnit.SECONDS);
        }

        if (count >= MAX_REQUESTS) {
            // Bloquear la IP en Redis por la duración especificada
            redisUtil.set(LOCKED_IPS_KEY + ip, "locked", LOCK_DURATION_SECONDS, TimeUnit.SECONDS);
            return false;
        }
        return true;
    }

    /**
     * Envía una respuesta JSON al cliente.
     * @param response El objeto HttpServletResponse.
     * @param apiResult El objeto ApiResult a serializar.
     * @throws IOException Si ocurre un error de I/O.
     */
    private void sendJsonResponse(HttpServletResponse response, ApiResult apiResult) throws IOException {
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json; charset=utf-8");
        ObjectMapper mapper = new ObjectMapper();
        try (PrintWriter writer = response.getWriter()) {
            writer.print(mapper.writeValueAsString(apiResult));
        } catch (IOException e) {
            log.error("Error al escribir respuesta JSON: {}", e.getMessage(), e);
            throw e;
        }
    }
}

El código anterior utiliza Redis para gestionar los bloqueos y contadores de solicitudes. La lógica de bloqueo se implementa mediante una clave Redis que almacena temporalmente las IPs bloqueadas, junto con un contador para cada combinación de IP y URI. Si el contador alcanza un umbral predefinido (MAX_REQUESTS), la IP se marca como bloqueada (LOCKED_IPS_KEY) durante un período específico (LOCK_DURATION_SECONDS).

El RedisUtil (asumiendo su existencia y funcionalidad correcta) es crucial para interactuar con Redis. Aquí se muestra un ejemplo simplificado de cómo podría ser este RedisUtil, adaptado para el uso con Spring Data Redis y patrones de bloqueo más convencionales:

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

/**
 * Utilidad para operaciones comunes de Redis.
 */
@Component
@Slf4j
public class RedisUtil {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * Incrementa el valor de una clave numérica en Redis.
     * @param key La clave a incrementar.
     * @param increment El valor a sumar.
     * @return El nuevo valor de la clave después de la operación.
     */
    public Long increment(String key, Long increment) {
        ValueOperations<String, Object> ops = redisTemplate.opsForValue();
        return ops.increment(key, increment);
    }

    /**
     * Establece el valor de una clave en Redis con un tiempo de expiración.
     * @param key La clave a establecer.
     * @param value El valor a almacenar.
     * @param timeout El tiempo de expiración.
     * @param unit La unidad de tiempo.
     */
    public void set(String key, Object value, long timeout, TimeUnit unit) {
        ValueOperations<String, Object> ops = redisTemplate.opsForValue();
        ops.set(key, value, timeout, unit);
        log.debug("Establecida clave '{}' con valor '{}' y expiración de {} {}", key, value, timeout, unit);
    }

    /**
     * Verifica si una clave existe en Redis.
     * @param key La clave a verificar.
     * @return true si la clave existe, false en caso contrario.
     */
    public boolean hasKey(String key) {
        return redisTemplate.hasKey(key);
    }

    /**
     * Establece el tiempo de expiración de una clave existente.
     * @param key La clave cuyo tiempo de expiración se modificará.
     * @param timeout El nuevo tiempo de expiración.
     * @param unit La unidad de tiempo.
     * @return true si la operación fue exitosa, false en caso contrario.
     */
    public boolean expire(String key, long timeout, TimeUnit unit) {
        return redisTemplate.expire(key, timeout, unit);
    }

    // Métodos adicionales como `get`, `delete`, etc., podrían ser añadidos si son necesarios.
}

Finalmente, para que el interceptor personalizado funcione, debes registrarlo en la configuración de tu aplicación web. Esto se hace usualmente extendiendo WebMvcConfigurerAdapter (o WebMvcConfigurer en Spring Boot 2.x+) y añadiendo el interceptor a la lista de interceptores:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * Configuración de la aplicación web para registrar interceptores.
 */
@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Bean
    public RateLimitInterceptor rateLimitInterceptor() {
        return new RateLimitInterceptor();
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // Registra el interceptor para todas las rutas
        registry.addInterceptor(rateLimitInterceptor())
                .addPathPatterns("/**");
    }
}

Etiquetas: Spring Boot seguridad Rate Limiting interceptor Redis

Publicado el 6-10 06:05