解决问题

目前这个博客套了阿里云和腾讯云的 CDN,忽然意识到,如果不设置请求头,Typecho 的后端可能没办法获取到正确的 IP,都是阿里云或者腾讯云的回源节点

我用的是 Caddy,官方文档看着有点懵,不如直接参考 Cloudflare 官方给的示例:Restoring original visitor IPs · Cloudflare Support docs

配置文件大概是这样的:

https://example.com {
    reverse_proxy localhost:8080 {
        # Sets X-Forwarded-For as the value Cloudflare gives us for CF-Connecting-IP.
        header_up X-Forwarded-For {http.request.header.CF-Connecting-IP}
    }
}

CF-Connecting-IP 是 Cloudflare 他们自己弄的头,我用的是阿里云 ESA,这里可以自定义标头,默认为 ali-real-client-ip,然后我自定义为 X-Forwarded-For,因为腾讯云默认里面为 X-Forwarded-For

之后我的 Caddy 配置文件为:

www.juniortree.com {
    root * /var/www/typecho
    encode gzip zstd
    php_fastcgi unix//run/php/php8.2-fpm.sock {
        env REMOTE_ADDR {http.request.header.x-forwarded-for}
    }
    file_server
}

但是这样还是不能解决问题,你现在测试还是会发现,访客的 IP 还是 CDN 回源节点的 IP,此时需要对 Typecho 的配置文件做些许修改

我找了几个比较热门的,都是说在 config.inc.php 最后添加:

//防止 CDN 造成无法获取客户真实 IP 地址
if(isset($_SERVER['HTTP_X_FORWARDED_FOR']))
{
    $list = explode(',',$_SERVER['HTTP_X_FORWARDED_FOR']);
    $_SERVER['REMOTE_ADDR'] = $list[0];
}

但是别人用了都说好,我用就是不舒服,所以重新找了个大佬的教程:【原创】Typecho 在使用 CDN 的情况下获取真实客户端 IP(非覆盖 $\_SERVER) - 青石坞

在配置文件 (config.inc.php) 中加入如下配置:

// 定义 IP 来源
define('__TYPECHO_IP_SOURCE__', 'HTTP_X_FORWARDED_FOR');

此时就能获得正确 IP 了

延伸

但,目前这样是有问题的

任何客户端都可以自定义 X-Forwarded-For 头部,例如通过 curl:

curl -H "X-Forwarded-For: 1.2.3.4" https://www.juniortree.com

所以最好的方法是你去定义一个自定义的,或者使用 CDN 服务商默认提供给你的,CDN 在收到用户请求后,会把真实用户 IP 提取出来(从 TCP 层),然后由系统在请求头中插入如 Ali-CDN-Real-IPCF-Connecting-IP 等字段,并传给你的源站服务器

比如说,我前面用的是阿里云和腾讯云的 CDN,他们都提供了自定义的方法,我就自定义一个我自己的请求头:

之后我写了一个 php 页面,尝试来打印一下请求头+服务器变量

<?php
header('Content-Type: text/plain');

// 打印 $_SERVER 中的和 IP、请求头相关的信息
echo "==== _SERVER ====\n";
$ip_keys = [
    'REMOTE_ADDR',
    'HTTP_X_FORWARDED_FOR',
    'HTTP_X_REAL_IP',
    'HTTP_CF_CONNECTING_IP',
    'HTTP_ALI_CDN_REAL_IP',
    'HTTP_X_CLIENT_IP',
];
foreach ($ip_keys as $key) {
    if (isset($_SERVER[$key])) {
        echo "$key: {$_SERVER[$key]}\n";
    }
}

// 打印全部请求头(仅 PHP 7.3+)
echo "\n==== getallheaders() ====\n";
if (function_exists('getallheaders')) {
    foreach (getallheaders() as $name => $value) {
        echo "$name: $value\n";
    }
} else {
    echo "getallheaders() 不可用\n";
}
?>

第一次在 Caddy 里面我们使用 X-Forward-For,然后来尝试伪造:

liueic@HUAWEI-MateBook-Go-ARM-Version ~ % curl -s https://www.juniortree.com/test.php \   
  -H "X-Forwarded-For: 1.2.3.4" \
  -H "X-Real-IP: 5.6.7.8" \
  -H "CF-Connecting-IP: 9.9.9.9"

==== _SERVER ====
REMOTE_ADDR: 1.2.3.4, 124.127.xxx.xx
HTTP_X_FORWARDED_FOR: 121.89.xx.xxx
HTTP_X_REAL_IP: 5.6.7.8
HTTP_CF_CONNECTING_IP: 9.9.9.9

==== getallheaders() ====
X-Real-Ip: 5.6.7.8
Accept: */*
X-Forwarded-Proto: https
Ali-Real-Client-Ip: 124.127.xxx.xx
Cdn-Loop: esa;loop=1
Ali-Ip-Country: CN
Content-Type: 
Eagleeye-Traceid: 
X-Forwarded-For: 121.89.xx.xxx
Ali-Ip-City: Beijing
User-Agent: curl/8.7.1
Content-Length: 0
Via: 2.0 Caddy
Host: www.juniortree.com
X-Forwarded-Host: www.juniortree.com

返回结果 REMOTE_ADDR: 1.2.3.4, 124.127.203.125,其中带有 1.2.3.4,说明被成功伪造了

之后在 Caddy 里面使用我们在 CDN 中自定义的头部:

liueic@HUAWEI-MateBook-Go-ARM-Version ~ % curl -s https://www.juniortree.com/test.php \
  -H "X-Forwarded-For: 1.2.3.4" \
  -H "Tree-Real-Client-Ip: 5.6.7.8"
==== _SERVER ====
REMOTE_ADDR: 124.127.xxx.xx
HTTP_X_FORWARDED_FOR: 121.89.xxx.xx

==== getallheaders() ====
X-Forwarded-Host: www.juniortree.com
Eagleeye-Traceid: 
Accept: */*
Via: 2.0 Caddy
Content-Type: 
X-Forwarded-Proto: https
Content-Length: 0
X-Forwarded-For: 121.89.xxx.xx
Host: www.juniortree.com
Ali-Ip-City: Beijing
Tree-Real-Client-Ip: 124.127.xxx.xx
Ali-Ip-Country: CN
Cdn-Loop: esa;loop=1
User-Agent: curl/8.7.1

此时并没有被成功伪造,而是被覆写了,这样的方式更安全,尤其是在你有审计或者 WAF 的情况下

最后修改:2025 年 07 月 01 日
如果觉得我的文章对你有用,请随意赞赏