ClientIpResolver.java
package org.darkroomlibrary.infrastructure.security;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.List;
/**
* Resolves the request source consistently for ingress limits and login protection.
*/
@Component
public class ClientIpResolver {
private static final String UNKNOWN = "unknown";
private final boolean trustForwardedHeaders;
private final List<CidrBlock> trustedProxyCidrs;
public ClientIpResolver(
@Value("${security.client-ip.trust-forwarded-headers:false}") boolean trustForwardedHeaders,
@Value("${security.client-ip.trusted-proxy-cidrs:}") String trustedProxyCidrs) {
this.trustForwardedHeaders = trustForwardedHeaders;
this.trustedProxyCidrs = parseCidrs(trustedProxyCidrs);
if (trustForwardedHeaders && this.trustedProxyCidrs.isEmpty()) {
throw new IllegalStateException("启用转发请求头时必须配置受信代理 CIDR 白名单");
}
}
public String resolve(HttpServletRequest request) {
if (request == null) {
return UNKNOWN;
}
String remoteAddress = normalizedAddress(request.getRemoteAddr());
if (remoteAddress == null) {
return UNKNOWN;
}
if (!trustForwardedHeaders || !isTrustedProxy(remoteAddress)) {
return remoteAddress;
}
String forwardedHeader = request.getHeader("X-Forwarded-For");
if (forwardedHeader != null && !forwardedHeader.isBlank()) {
List<String> forwardedChain = parseForwardedChain(forwardedHeader);
if (forwardedChain.isEmpty()) {
return remoteAddress;
}
for (int i = forwardedChain.size() - 1; i >= 0; i--) {
String address = forwardedChain.get(i);
if (!isTrustedProxy(address)) {
return address;
}
}
return forwardedChain.get(0);
}
String realIp = normalizedAddress(request.getHeader("X-Real-IP"));
return realIp == null ? remoteAddress : realIp;
}
public String resolveCurrentRequest() {
if (RequestContextHolder.getRequestAttributes() instanceof ServletRequestAttributes attributes) {
return resolve(attributes.getRequest());
}
return UNKNOWN;
}
private List<String> parseForwardedChain(String value) {
List<String> addresses = new ArrayList<>();
for (String part : value.split(",")) {
String address = normalizedAddress(part);
if (address == null) {
return List.of();
}
addresses.add(address);
}
return addresses;
}
private boolean isTrustedProxy(String address) {
byte[] bytes = addressBytes(address);
if (bytes == null) {
return false;
}
return trustedProxyCidrs.stream().anyMatch(cidr -> cidr.matches(bytes));
}
private static List<CidrBlock> parseCidrs(String configuredCidrs) {
if (configuredCidrs == null || configuredCidrs.isBlank()) {
return List.of();
}
List<CidrBlock> cidrs = new ArrayList<>();
for (String configuredCidr : configuredCidrs.split(",")) {
String value = configuredCidr.trim();
if (value.isEmpty()) {
continue;
}
cidrs.add(CidrBlock.parse(value));
}
return List.copyOf(cidrs);
}
private static String normalizedAddress(String value) {
if (value == null) {
return null;
}
String normalized = value.trim();
if (normalized.isEmpty() || "unknown".equalsIgnoreCase(normalized)) {
return null;
}
if (normalized.startsWith("[") && normalized.endsWith("]")) {
normalized = normalized.substring(1, normalized.length() - 1);
}
int zoneIndex = normalized.indexOf('%');
if (zoneIndex >= 0) {
normalized = normalized.substring(0, zoneIndex);
}
byte[] bytes = addressBytes(normalized);
if (bytes == null) {
return null;
}
try {
return InetAddress.getByAddress(bytes).getHostAddress();
} catch (UnknownHostException e) {
return null;
}
}
private static byte[] addressBytes(String value) {
if (value == null || value.isBlank()) {
return null;
}
if (value.indexOf(':') < 0 && !isIpv4Literal(value)) {
return null;
}
if (value.indexOf(':') >= 0 && !value.matches("[0-9a-fA-F:.]+")) {
return null;
}
try {
return InetAddress.getByName(value).getAddress();
} catch (UnknownHostException e) {
return null;
}
}
private static boolean isIpv4Literal(String value) {
String[] parts = value.split("\\.", -1);
if (parts.length != 4) {
return false;
}
for (String part : parts) {
if (part.isEmpty() || part.length() > 3 || !part.chars().allMatch(Character::isDigit)) {
return false;
}
int number = Integer.parseInt(part);
if (number > 255) {
return false;
}
}
return true;
}
private record CidrBlock(byte[] network, int prefixLength) {
private static CidrBlock parse(String value) {
String[] parts = value.split("/", -1);
if (parts.length > 2) {
throw new IllegalArgumentException("无效的受信代理 CIDR: " + value);
}
String address = normalizedAddress(parts[0]);
byte[] bytes = addressBytes(address);
if (bytes == null) {
throw new IllegalArgumentException("无效的受信代理 CIDR: " + value);
}
int maxPrefix = bytes.length * Byte.SIZE;
int prefix = parts.length == 1 ? maxPrefix : parsePrefix(parts[1], maxPrefix, value);
byte[] network = bytes.clone();
clearHostBits(network, prefix);
return new CidrBlock(network, prefix);
}
private boolean matches(byte[] candidate) {
if (candidate.length != network.length) {
return false;
}
int fullBytes = prefixLength / Byte.SIZE;
int remainingBits = prefixLength % Byte.SIZE;
for (int i = 0; i < fullBytes; i++) {
if (candidate[i] != network[i]) {
return false;
}
}
if (remainingBits == 0) {
return true;
}
int mask = 0xFF << (Byte.SIZE - remainingBits);
return (candidate[fullBytes] & mask) == (network[fullBytes] & mask);
}
private static int parsePrefix(String value, int maxPrefix, String cidr) {
try {
int prefix = Integer.parseInt(value);
if (prefix < 0 || prefix > maxPrefix) {
throw new IllegalArgumentException("无效的受信代理 CIDR: " + cidr);
}
return prefix;
} catch (NumberFormatException e) {
throw new IllegalArgumentException("无效的受信代理 CIDR: " + cidr, e);
}
}
private static void clearHostBits(byte[] address, int prefixLength) {
int fullBytes = prefixLength / Byte.SIZE;
int remainingBits = prefixLength % Byte.SIZE;
if (remainingBits > 0 && fullBytes < address.length) {
int mask = 0xFF << (Byte.SIZE - remainingBits);
address[fullBytes] = (byte) (address[fullBytes] & mask);
fullBytes++;
}
for (int i = fullBytes; i < address.length; i++) {
address[i] = 0;
}
}
}
}