如果Discuz站点放置在海外,当用户挂着梯子时,Discuz获得的IP是梯子节点IP。
如何获得用户真实的IP?通常用户挂梯子并非采用全局代理,而是访问海外站点时才走梯子,因此我们可以在中国大陆境内放置一个IP查询API,然后Discuz前端请求这个API获得用户的真实IP,写入Cookie供Discuz获取。
1、在境内服务器设置一个IP查询API(自行创建,如需帮助请联系我)。该API返回JSON,举例结构:
$result = [
'ip' => $ip,
'country' => $country,
'ts' => $ts,
'sign' => $sign,
];
其中 sign 值生成方式(与后续验证对应):
$ts = time(); //当前时间戳
$data = $ip.'|'.$country.'|'.$ts; //拼接数据
$fullSign = hash_hmac('sha256', $data, $signSecret);
$sign = substr($fullSign, 0, 12);
2、在 static/js/common.js 中增加一个函数,另外手机版的 common.js 也许加入。用于从API获取IP信息后写入Cookie。
function setRealIpCookie(apiUrl, autoReload) {
if (!apiUrl || !window.fetch) {
return;
}
var expires = new Date(Date.now() + 3 * 3600 * 1000).toUTCString();
fetch(apiUrl, { credentials: 'omit' })
.then(function (resp) { return resp.json(); })
.then(function (data) {
if (!data || !data.ip || !data.ts || !data.sign) {
return;
}
var ip = data.ip;
var country = data.country || 'other';
var ts = data.ts;
var sign = data.sign;
var value = [ip, country, ts, sign].join('|');
document.cookie =
'realip=' + encodeURIComponent(value) +
'; expires=' + expires +
'; path=/';
document.cookie =
'ipcountry=' + encodeURIComponent(country) +
'; expires=' + expires +
'; path=/';
if (autoReload) {
location.reload();
}
})
.catch(function () {});
}
3、在模板 common/footer (包括电脑版和手机版)底部增加:
<!--{if $_G['realipreq']}-->
<script type="text/javascript">setRealIpCookie('https://你的API地址');</script>
<!--{/if}-->
当自定义全局变量 $_G['realipreq'] 存在时,就触发API查询和写入Cookie的操作。$_G['realipreq'] 这个变量在下一步中设置。
部分页面需要立即根据API获取的IP做展示,比如注册页面有区分中国用户和海外用户,那么我们就需要在请求API设置Cookie后立即进行一次刷新操作,可以在这些页面将上述函数增加一个true参数,示例:setRealIpCookie('https://你的API地址', true);
4、打开 source/class/discuz/discuz_application.php ,查找 _get_client_ip() 函数,将其整体替换为:
private function _get_client_ip() {
if(!defined('REALIP_SIGN_SECRET')) {
define('REALIP_SIGN_SECRET', '自定义secret,须于API中一致,并且加密方式一致');
}
// 1. 原始 IP 获取
$headers = array(
'HTTP_X_REAL_IP',
'HTTP_X_FORWARDED_FOR',
'HTTP_CLIENT_IP',
'HTTP_CF_CONNECTING_IP',
'HTTP_CF_PSEUDO_IPV4',
'REMOTE_ADDR',
);
$ip = '0.0.0.0';
foreach($headers as $key) {
if(!empty($_SERVER[$key])
&& preg_match('/^([0-9]{1,3}\.){3}[0-9]{1,3}$/', $_SERVER[$key])
&& $_SERVER[$key] !== '127.0.0.1') {
$ip = $_SERVER[$key];
break;
}
}
$proxyip = $ip;
// 2. realip 校验逻辑
$needClearRealip = false;
if(!empty($_COOKIE['realip']) && REALIP_SIGN_SECRET) {
$parts = explode('|', $_COOKIE['realip']);
// 期望结构:ip|country|ts|sign
if(count($parts) === 4) {
list($rip, $rcountry, $rts, $rsign) = $parts;
if(preg_match('/^([0-9]{1,3}\.){3}[0-9]{1,3}$/', $rip)
&& $rip !== '127.0.0.1'
&& ctype_digit($rts)) {
$rts = intval($rts);
$now = defined('TIMESTAMP') ? TIMESTAMP : time();
// 3 小时有效 + 少量误差
if($rts > 0 && $now - $rts <= 3 * 3600 && $now - $rts >= -300) {
$rcountry = (string)$rcountry;
$data = $rip . '|' . $rcountry . '|' . $rts;
$fullSign = hash_hmac('sha256', $data, REALIP_SIGN_SECRET);
$calcSign = substr($fullSign, 0, 12);
$ok = function_exists('hash_equals')
? hash_equals($calcSign, $rsign)
: $calcSign === $rsign;
if($ok) {
// 验证通过,使用 Cookie 中的 IP
$ip = $rip;
if($proxyip && $proxyip !== $ip) {
global $_G;
$_G['proxyip'] = $proxyip;
}
} else {
// 签名不通过,标记清除
$needClearRealip = true;
}
} else {
// 时间戳过期,标记清除
$needClearRealip = true;
}
} else {
// 格式不对,标记清除
$needClearRealip = true;
}
} else {
// 结构不对,标记清除
$needClearRealip = true;
}
}
// 3. 如果需要,清除 realip Cookie
if($needClearRealip) {
// 清除当前作用域下的 Cookie
setcookie('realip', '', time() - 3600, '/');
// 同时更新超全局,避免本次请求后续逻辑再用到旧值
unset($_COOKIE['realip']);
}
return $ip;
}
这段代码从Cookie中读取IP,如果校验成功且在有效期内,则用该IP设置为客户端IP。否则仍然是直接获取IP。
万事俱备,只差一步设置 $_G['realipreq'] 触发前台去请求API的操作了。在上面的后面函数后新增:
private function _init_realip_request_flag() {
global $_G;
// 已经有真实 IP Cookie 的,不再请求
if (!empty($_COOKIE['realip'])) {
return;
}
// 1. 优先用用户级缓存:ipcountry Cookie
if (!empty($_COOKIE['ipcountry']) && $_COOKIE['ipcountry'] !== 'cn') {
$_G['realipreq'] = 1;
return;
}
// 2. 没有 ipcountry Cookie,首次判定 IP 归属地
include_once libfile('function/misc');
$ip = $_G['clientip'];
$location = convertip($ip);
if ($location && preg_match('/(电信|联通|移动|教育|鹏博士|长城|中国|北京|上海|天津|重庆|广东|福建|浙江|江苏|山东|山西|黑龙江|辽宁|吉林|河北|河南|湖北|湖南|安徽|江西|陕西|四川|贵州|云南|广西|海南|甘肃|宁夏|青海|新疆|内蒙古|西藏)/', $location)) {
$country = 'cn';
} else {
$country = 'other';
}
// 写入用户级缓存 Cookie,后续请求直接使用
$expire = time() + 3 * 3600; // 3小时,可自行调整
setcookie('ipcountry', $country, $expire, '/');
$_COOKIE['ipcountry'] = $country;
// 非中国 IP,标记需要前端请求真实 IP
if ($country !== 'cn') {
$_G['realipreq'] = 1;
}
}
只在IP不在中国、且 $_COOKIE['realip'] 不存在时才去请求API查询。
最后,需要查找:$this->var = & $_G; ,在下方增加:$this->_init_realip_request_flag();
若您喜欢这篇文章,欢迎订阅老张小站以获得最新内容。 / 欢迎交流探讨,请发电子邮件至 mail[at]vdazhang.com 。
欢迎谈谈你的看法(无须登录) *正文中请勿包含"http://"否则将被拦截