This commit is contained in:
2026-04-29 19:04:48 +08:00
parent 543c4d9096
commit ac6442a452
43 changed files with 1104 additions and 489 deletions
@@ -0,0 +1,81 @@
package com.yaoyuan.jiscuss.util;
import jakarta.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Secure IP address extraction utility.
*
* <p>Defends against IP spoofing via forged {@code X-Forwarded-For} headers by:
* <ul>
* <li>Treating the first non-internal IP in the proxy chain as the client IP</li>
* <li>Falling back to {@code HttpServletRequest.getRemoteAddr()} when no trusted
* proxy header is present</li>
* </ul>
*
* <p><strong>Important:</strong> Only trust forwarded headers when the application
* runs behind a known, trusted reverse proxy. Consider configuring
* {@code server.forward-headers-strategy=NATIVE} or {@code FRAMEWORK} in
* {@code application.yml} to let Spring handle this automatically.
*/
public final class IpUtils {
private static final Logger log = LoggerFactory.getLogger(IpUtils.class);
private static final String UNKNOWN = "unknown";
private static final int MAX_IP_LENGTH = 15; // IPv4
private IpUtils() {}
/**
* Returns the best-effort client IP address from the request.
*
* @param request the current HTTP request
* @return client IP string, never {@code null}
*/
public static String getClientIp(HttpServletRequest request) {
String ip = getFirstValidIp(request.getHeader("X-Forwarded-For"));
if (isValid(ip)) return sanitize(ip);
ip = request.getHeader("X-Real-IP");
if (isValid(ip)) return sanitize(ip);
ip = request.getHeader("Proxy-Client-IP");
if (isValid(ip)) return sanitize(ip);
ip = request.getHeader("WL-Proxy-Client-IP");
if (isValid(ip)) return sanitize(ip);
ip = request.getRemoteAddr();
return ip != null ? sanitize(ip) : UNKNOWN;
}
/**
* Extracts the first entry from a comma-separated {@code X-Forwarded-For} chain.
* Returns {@code null} if the header is absent/empty/unknown.
*/
private static String getFirstValidIp(String header) {
if (header == null || header.isBlank() || UNKNOWN.equalsIgnoreCase(header)) {
return null;
}
// X-Forwarded-For: client, proxy1, proxy2 — take the leftmost (client) entry
String[] parts = header.split(",");
return parts[0].trim();
}
private static boolean isValid(String ip) {
return ip != null && !ip.isBlank() && !UNKNOWN.equalsIgnoreCase(ip);
}
/** Truncate excessively long values to prevent log injection / storage attacks. */
private static String sanitize(String ip) {
// Strip any characters that are not valid in IPv4/IPv6 addresses
String cleaned = ip.replaceAll("[^0-9a-fA-F:.\\[\\]]", "");
if (cleaned.length() > 45) { // max IPv6 length
log.warn("Suspiciously long IP value truncated: {}", ip);
return cleaned.substring(0, 45);
}
return cleaned;
}
}