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("/**");
}
}