要在日志中记录操作人的真实IP,核心是区分部署场景(直连/反向代理) 并正确提取IP(处理代理转发的请求头),以下分「通用原理」「主流语言/框架实现」「注意事项」三部分说明:
一、通用原理
| 部署场景 | 核心逻辑 |
|---|---|
| 直连应用服务器 | 直接获取TCP连接的远端IP(如 REMOTE_ADDR) |
| 反向代理(Nginx/CDN/APACHE) | 代理会将真实客户端IP写入请求头(如 X-Forwarded-For/X-Real-IP),需优先从这些头提取 |
关键请求头说明:
X-Forwarded-For:格式为客户端IP, 代理1IP, 代理2IP(多个代理时,第一个是真实客户端IP);X-Real-IP:直接记录真实客户端IP(单代理场景常用);REMOTE_ADDR:默认是「直接连接应用的节点IP」(代理场景下是代理IP,非客户端真实IP)。
二、主流语言/框架实现
1. Java(Spring Boot/Spring MVC)
步骤1:配置Nginx(代理场景)
若使用Nginx反向代理,先在 nginx.conf中添加请求头转发:
server {
listen 80;
server_name your-domain.com;
# 转发真实IP到应用
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
location / {
proxy_pass http://127.0.0.1:8080; # 应用地址
}
}
步骤2:封装IP获取工具类
import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Pattern;
public class IpUtils {
// 排除内网/保留IP(可选,根据业务需要)
private static final Pattern INNER_IP_PATTERN = Pattern.compile(
"^(127\\.0\\.0\\.1)|(localhost)|(10\\.\\d+\\.\\d+\\.\\d+)|(172\\.((1[6-9])|(2\\d)|(3[0-1]))\\.\\d+\\.\\d+)|(192\\.168\\.\\d+\\.\\d+)|(::1)|(fe80::.*)$"
);
/**
* 获取真实客户端IP
*/
public static String getRealIp(HttpServletRequest request) {
// 1. 优先取X-Forwarded-For
String xff = request.getHeader("X-Forwarded-For");
if (isValidIp(xff)) {
// 拆分逗号分隔的IP列表,取第一个有效IP
List<String> ipList = Arrays.asList(xff.split(","));
for (String ip : ipList) {
ip = ip.trim();
if (isValidIp(ip) && !INNER_IP_PATTERN.matcher(ip).matches()) {
return ip;
}
}
}
// 2. 取X-Real-IP
String xri = request.getHeader("X-Real-IP");
if (isValidIp(xri)) {
return xri.trim();
}
// 3. 最后取REMOTE_ADDR
return request.getRemoteAddr();
}
/**
* 校验IP是否有效(非空/非unknown)
*/
private static boolean isValidIp(String ip) {
return ip != null && !ip.isEmpty() && !"unknown".equalsIgnoreCase(ip);
}
}
步骤3:在业务中使用(记录日志)
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@RestController
public class OperationController {
private static final Logger log = LoggerFactory.getLogger(OperationController.class);
@GetMapping("/operate")
public String operate(HttpServletRequest request) {
String realIp = IpUtils.getRealIp(request);
// 记录到日志(可结合MDC实现全链路日志关联)
log.info("操作人IP:{},执行了XXX操作", realIp);
return "success";
}
}
2. Python
(1)Django
import re
from django.http import HttpRequest
# 内网IP正则
INNER_IP_PATTERN = re.compile(
r"^(127\.0\.0\.1)|(localhost)|(10\.\d+\.\d+\.\d+)|(172\.((1[6-9])|(2\d)|(3[0-1]))\.\d+\.\d+)|(192\.168\.\d+\.\d+)|(::1)|(fe80::.*)$"
)
def get_real_ip(request: HttpRequest) -> str:
"""获取真实IP"""
# 1. 优先取X-Forwarded-For
xff = request.META.get('HTTP_X_FORWARDED_FOR', '')
if xff and xff != 'unknown':
# 拆分第一个有效IP
ip = xff.split(',')[0].strip()
if ip and not INNER_IP_PATTERN.match(ip):
return ip
# 2. 取X-Real-IP
xri = request.META.get('HTTP_X_REAL_IP', '')
if xri and xri != 'unknown':
return xri.strip()
# 3. 最后取REMOTE_ADDR
return request.META.get('REMOTE_ADDR', 'unknown')
# 业务中使用(记录日志)
import logging
logger = logging.getLogger(__name__)
def operate(request):
real_ip = get_real_ip(request)
logger.info(f"操作人IP:{real_ip},执行了XXX操作")
return {"code": 200, "msg": "success"}
(2)Flask
import re
from flask import request
import logging
logger = logging.getLogger(__name__)
INNER_IP_PATTERN = re.compile(
r"^(127\.0\.0\.1)|(localhost)|(10\.\d+\.\d+\.\d+)|(172\.((1[6-9])|(2\d)|(3[0-1]))\.\d+\.\d+)|(192\.168\.\d+\.\d+)|(::1)|(fe80::.*)$"
)
def get_real_ip():
"""获取真实IP"""
# 1. X-Forwarded-For
xff = request.headers.get('X-Forwarded-For', '')
if xff and xff != 'unknown':
ip = xff.split(',')[0].strip()
if ip and not INNER_IP_PATTERN.match(ip):
return ip
# 2. X-Real-IP
xri = request.headers.get('X-Real-IP', '')
if xri and xri != 'unknown':
return xri.strip()
# 3. remote_addr
return request.remote_addr
# 接口中使用
@app.route('/operate')
def operate():
real_ip = get_real_ip()
logger.info(f"操作人IP:{real_ip},执行了XXX操作")
return "success"
3. Node.js(Express)
const express = require('express');
const app = express();
const logger = require('winston'); // 或其他日志库
// 信任代理(关键:让Express识别代理转发的IP)
app.set('trust proxy', true); // 生产环境建议指定代理IP段,如 ['192.168.1.0/24']
// 内网IP校验
const isInnerIp = (ip) => {
return /^(127\.0\.0\.1)|(localhost)|(10\.\d+\.\d+\.\d+)|(172\.((1[6-9])|(2\d)|(3[0-1]))\.\d+\.\d+)|(192\.168\.\d+\.\d+)|(::1)|(fe80::.*)$/.test(ip);
};
// 获取真实IP
const getRealIp = (req) => {
// 1. X-Forwarded-For
let xff = req.headers['x-forwarded-for'];
if (xff) {
const ipList = xff.split(',').map(ip => ip.trim());
for (const ip of ipList) {
if (ip && !isInnerIp(ip)) {
return ip;
}
}
}
// 2. X-Real-IP
let xri = req.headers['x-real-ip'];
if (xri && !isInnerIp(xri)) {
return xri;
}
// 3. req.ip(Express trust proxy后自动处理)
return req.ip;
};
// 业务接口
app.get('/operate', (req, res) => {
const realIp = getRealIp(req);
logger.info(`操作人IP:${realIp},执行了XXX操作`);
res.send('success');
});
app.listen(3000);
4. PHP
<?php
/**
* 获取真实IP
*/
function getRealIp() {
// 内网IP正则
$innerIpPattern = '/^(127\.0\.0\.1)|(localhost)|(10\.\d+\.\d+\.\d+)|(172\.((1[6-9])|(2\d)|(3[0-1]))\.\d+\.\d+)|(192\.168\.\d+\.\d+)|(::1)|(fe80::.*)$/';
// 1. X-Forwarded-For
$xff = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? '';
if ($xff && $xff !== 'unknown') {
$ipList = explode(',', $xff);
$ip = trim($ipList[0]);
if ($ip && !preg_match($innerIpPattern, $ip)) {
return $ip;
}
}
// 2. X-Real-IP
$xri = $_SERVER['HTTP_X_REAL_IP'] ?? '';
if ($xri && $xri !== 'unknown') {
return trim($xri);
}
// 3. REMOTE_ADDR
return $_SERVER['REMOTE_ADDR'] ?? 'unknown';
}
// 记录日志
$realIp = getRealIp();
error_log("操作人IP:{$realIp},执行了XXX操作");
echo "success";
?>
三、关键注意事项
-
防止IP伪造:
- 生产环境需限制「仅信任前端代理的IP段」(如Spring Boot配置
server.forward-headers-strategy=native、Express指定trust proxy: ['192.168.1.0/24']),避免恶意用户伪造X-Forwarded-For头; - 不要直接使用
X-Forwarded-For的第一个值,需过滤内网IP/无效值。
- 生产环境需限制「仅信任前端代理的IP段」(如Spring Boot配置
-
IPv6兼容:
- 注意IPv6地址格式(如
::1是本地回环,240e::xxx是公网IPv6),正则需包含IPv6规则。
- 注意IPv6地址格式(如
-
日志规范:
- 建议将IP放入日志的结构化字段(如JSON格式的
client_ip),便于后续日志分析/检索; - 结合MDC(Java)/context(Python)实现「请求ID+IP+操作」全链路关联。
- 建议将IP放入日志的结构化字段(如JSON格式的
-
特殊场景:
- 客户端使用代理/VPN时,获取的是代理/VPN的IP(无法获取真实物理IP,属于正常现象);
- 内网系统可忽略公网IP校验,直接记录内网IP即可。