老张小站

  1. 欢迎光临

    感谢访问老张的博客!

  • 1
42

Discuz! X 海外站点绕过梯子获取真实IP的方法

分类 网站技术/村民张先生 发布于 2025-11-18 07:04
0

如果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();

欢迎转载分享,转载请注明 来源:大张小站 https://www.zhang.cq.cn/20252478.html
若您喜欢这篇文章,欢迎订阅老张小站以获得最新内容。 / 欢迎交流探讨,请发电子邮件至 mail[at]vdazhang.com 。


欢迎谈谈你的看法(无须登录) *正文中请勿包含"http://"否则将被拦截