一、先有前提,再谈策略

给协会的中转站做安全收口,第一步是翻日志。

中转站跑在阿里云上,域名是 api.liuhangyv.top。协会的中转站,负责把成员发起的请求转发到上游。我准备给它补一套安全策略:谁在什么时候调用了什么,要留得下痕迹;按来源做限流;给 API Key 配一份 IP 白名单。

三件事拆开各是一处配置,指向的却是同一个前提——源站得知道来的人是谁。源站就是真正跑业务的那台服务器。

于是我翻开审计日志,想挑一条记录核对。客户端 IP 那一列,我停住了:整列都是 47.123.117.x 这样的地址。

那不是成员家里的宽带,也不是谁在宿舍连的校园网,是机房段里的一台机器。那一列里读不出任何关于「谁」的信息。

QQ20261001-201700-1790857037483.webp

这不是我哪一步配错了

我先是怀疑自己哪里写错了。后来才想明白,这是请求经过 CDN 之后必然的结果,跟我怎么配没关系。

请求先到边缘节点,再由边缘节点回源——回源就是从边缘节点回到源站去取内容。这条链路上,跟源站建立连接的是边缘节点的回源出口,不是发起请求的那个成员。所以 nginx 里的 $remote_addr,记的是那条连接的对面是谁,跟真正发起请求的人无关。

任何 CDN 都这样,跟选的是哪家无关,是链路结构决定的。

链路示意图生成-1790857247729.webp

麻烦的地方在补不回来

如果只是记了个错值,换一种采集方式就能修。真正的问题是,这个值补不回来:边缘回源时走哪个出口,跟发起请求的那个人之间没有对应关系,这一跳过去,信息就没了。

整个过程不出声,也没有任何地方会提醒你,日志照常一行行写进去。要不是我拿着一条记录去核对,这件事不会被谁发现。

请求头里倒是有个 X-Forwarded-For,它带着真实客户端 IP。可这个头谁都能自己写——源站凭什么信它?

问题不是日志里记错了 IP,而是它记的那个 IP,跟你没有任何关系。

所以在写策略之前,得先把源站手里的那份身份信息变回真的——身份是假的,往上加多少规则都白搭。

二、边缘替源站盖的三个章

源站要知道来的人是谁,阿里云 ESA(边缘安全加速)给的答案很具体:三个由边缘节点自己写进去的请求头。

三个开关,勾一次就在了

控制台里的路径是「转换规则 → 托管转换」,面板上三个开关,勾上、保存,没有别的事要做。

名字起得很直白:添加真实客户端 IP 标头、添加访问者位置标头、添加安全请求头。

往请求里加头这件事本身不算新鲜,各家 CDN 都有办法做到,通常是让你在边缘规则里自己拼一条——写在哪、叫什么、什么条件下发,全归你管。ESA 把这三样做成了产品里的开关,选一下就在了。

只开第一个也够用,另外两个是顺手加的——都是现成的开关,多开两个,不多一处要维护的配置。

QQ20261001-201949-1790857201775.webp

源站收到的是哪几个头

三个都打开之后,我在源站侧把请求头打出来看了一遍:

开关实际注入的头拿到什么
添加真实客户端 IP 标头ali-real-client-ip真实客户端 IP
添加访问者位置标头ali-ip-country、ali-ip-cityCN + 一个地级市名
添加安全请求头Tls-Hash32 位 MD5(JA3 指纹)

QQ20261001-201949-1790857201775.webp

这几个名字值得多看一眼。它们没有借用通用的 X-Forwarded-For,而是走了厂商自己的命名空间——ali- 打头、Tls- 打头。这个选择,把下面这件事讲明白了。

这几个头,客户端塞不进去

验证的办法很直接:带上一个伪造值发一次请求,再看源站收到了什么:

经 ESA 访问,同时伪造 ali-real-client-ip / ali-ip-country / Tls-Hash
  → 源站收到的是 ESA 注入的值,伪造的那份被覆盖掉了

问题只有一个:客户端能不能决定源站最终看到的值。答案是不能。

经 ESA 进来的请求,这几个头客户端改不了。这个结论的适用范围是「请求确实从 ESA 过」——凡是把信任押在代理身上的做法,都绕不开这条边界。

信任是怎么建立的

X-Forwarded-For(下称 XFF)是 HTTP 里的事实标准,任何客户端都能自己塞一个进去。HTTP 头本来就可以随便追加,协议里也没有签名字段,能证明某一条出自谁手。源站敢信 XFF,靠的是额外的一条配置:只认自己代理网段里来的地址,别处写的一律不认。名字里带个 X-,说明不了任何问题。

ali-real-client-ip 的路子不一样。它由边缘节点生成,客户端就算提前放一个同名的头上去,到了边缘也会被冲掉。两者的差别落在信任建立的环节上:XFF 是源站事后拿一份名单去校,名单写宽了就会漏;而边缘写下的那个头,作者在写下去的那一刻就已经定死了。

像一张表上有两栏:一栏当事人自己填,另一栏由工作人员核对证件后录入。X-Forwarded-For 是自己填的那栏,ali-real-client-ip 是录入的那栏——填得再像样,也还是他自己写的。

一个头能不能信,不看它叫什么名字,看它是谁写上去的。

Tls-Hash 是个什么头

它是 JA3 指纹:把 TLS 握手的参数按固定顺序拼起来算出来的一个哈希。同一台设备换了 IP,指纹还是那个指纹,所以可以在不看请求内容的前提下,认出「这是不是同一个客户端」。

这里有一处和文档对不上。阿里云文档里写的是 Tls-Ja3 / Tls-Ja4,我照着去接,源站一条都收不到;实际注入的是 Tls-Hash。我没照文档抄,是逐个验出来的——顺手建议官方把文档这处补一下。

源站那边加三行 nginx

之前源站看到的是回源 IP,现在改成读 ESA 注入的那个头:

set_real_ip_from <ESA 回源网段>;
real_ip_header ali-real-client-ip;
real_ip_recursive on;

三行里第一行最容易漏。

set_real_ip_from 回答的是「谁有资格改写客户端地址」——只有当连接的来源地址落在这份名单里,nginx 才会去读那个头。名单空着的时候它直接跳过,下面两行写了也白写。前面说的「只信自己的代理网段」,落到配置里就是这一行。这份清单在 ESA 控制台里能拿到,我这边是 258 条(149 个 IPv4 段 + 109 个 IPv6 段)。

第三行 real_ip_recursive on 管的是头里带了一长串地址时该取哪一个。ali-real-client-ip 只有一个地址,所以这一行对本例没有影响——它是给 XFF 那类可能被追加成一串的头准备的。

配完,源站日志里的 $remote_addr 就是真实客户端 IP,写进审计表的也是它。

改完别只看配置文件,拿一条真实请求去日志里验一遍:

curl -s -o /dev/null -w "%{http_code}" "https://<站点>/login?probe=check-$(date +%H%M%S)"
sudo grep -a 'check-<时间>' <access.log 路径> | awk '{print $1}'

这段是让你拿一次自己刚发出去的请求,回日志里看 nginx 记下的地址对不对。打出来是自己的公网 IP,才算这条链路真的通了。

这里有个小坑:grep 得加 -a,access.log 里含二进制字节,不加这个参数,它会把文件整个判成二进制,什么都不输出。

这几行生效之后,源站日志里的客户端地址,写的就是真正发起请求的那个人了。

三、拿到手之后,这三个头能干什么

三个开关都已经生效,源站侧该接的也接上了。只是把它当成一个修好的开关,有点浪费——这三个头还各自答着别的问题。

前面提过的那三件事——留痕、限流、白名单——到这里不用再单独做什么,它们就是这套配置的本职。

地理位置:审计多一个维度

ali-ip-country 给国家代码,ali-ip-city 给一个地级市名。

IP 地理库就是一份「哪个 IP 段属于哪个地方」的映射表,一般得单独买或者用开源的。接进来还不能放着不管——IP 段的归属一直在变,运营商会调整、地址会被重新分配,这份表是有保质期的,不跟着更新就会慢慢失准。

ESA 是在边缘侧把位置算好,塞进请求头再回源。源站侧收到的就是 CN 加一个城市名,日志格式里多两列的事,不用装库,也不用维护那份会过期的表。

审计里从此多出一个维度:同一条 API Key 的调用来自哪些城市,某个账号的轨迹有没有突然换一片地方。这类问题以前要么做不了,要么得自己先搭一套。

JA3 指纹:换个 IP 也认得出来

Tls-Hash 是什么,前面讲过,这里只说能拿它干什么。

设备指纹不是登录态,也不是 Cookie,是从客户端连接自身的特征里算出来的标记,用来认出「是不是一个熟悉的客户端」,不需要读请求内容。

JA3 算的是 TLS 握手里的参数,跟 IP 无关。同一个客户端换个网络、换个出口 IP,算出来的值不变。当它换了一串 IP 还在调同一个接口时,用 IP 看是很多个来源,用指纹看是同一个客户端。

对风控来说这是现成的补充信号:没见过的指纹突然开始高频调用,或者同一个指纹挂着一串不相干的城市,都可以是多看一眼的理由。

它认的是客户端这一侧的握手特征,同一款客户端程序在不同机器上可能算出同一个值——不是设备编号,但分辨老面孔和生面孔够用。

这几个头都是在边缘算好的,源站接过来就行——不用自己维护一份会过期的判断依据。

四、这套东西在真实流量里长什么样

先把口径交代清楚

数据从 ESA 控制台导出,站点 liuhangyv.top,区间 2026-09-21 00:00:00 到 2026-09-27 23:59:59,时区 +08:00,整整 7 天。这个域名接进来已经两个月,这里取的是最近一周。

一周约 28.7 万次请求,日均约 4.1 万。量级不大,就是个人站点加几个自用服务的水平。好处是它不是空转的——每一组数背后都有真实的人在访问。

QQ20261004-204458-1791117907350.webp

缓存那一半,先看口径

ESA 给的可缓存命中率是 76.9%。

这个数有它自己的算法,不说清楚容易看歪:分母只算被判定为可缓存的那部分请求,也就是 HIT ÷ (HIT + MISS)。每次都得回源、本来就不可能命中的请求会被判成 DYNAMIC,不参与这个除法。换成全站请求做分母,数字会难看不少,可那量的是另一回事。

一周的缓存状态分布是这样:

缓存状态请求数
DYNAMIC15.8 万
NONE / UNKNOWN5 万
HIT3.46 万
BYPASS3.27 万
MISS1.04 万

DYNAMIC 占了一半还多,这没什么奇怪:博客的 HTML、接口请求、登录跳转本来就该走动态。真正落在可缓存区间里的那部分,四分之三被边缘直接接走了。

按域名看,这堆流量都是谁的

这张表是这一节我最想给你看的:

Host请求数
blog.liuhangyv.top7.75 万
api.liuhangyv.top2.48 万
gitea.liuhangyv.top1.43 万
liuhangyv.top1.2 万
www.liuhangyv.top1.14 万

QQ20261004-204543-1791117955188.webp

博客第一,意料之中。中转站 api.liuhangyv.top 排在第二,这个位置我事先没料到。

但我不想把它说大:2.48 万除以 28.7 万,大概 8.6%,连总量的十分之一都不到。它能排到第二,是因为这个站上其他域名都不大。

它体量不大,可它每一条请求都要求可信身份。博客被谁看了无所谓,Git 仓库被谁 clone 了也无所谓;中转站不行,每一条都得知道是谁发的、从哪来的。小流量加高安全要求,这件事本身就是把它放到边缘后面的理由。

几项技术观察

协议版本这项,HTTP/1.1 19.76 万、HTTP/2.0 7.02 万,另有 1.9 万次走的是 HTTP/3。也就是说边缘侧支持 QUIC,将近两万次请求是从这条路上过来的。

另一项是指纹:控制台里带 JA3 指纹的请求有 23.95 万次,无指纹的 4.74 万。前面说 Tls-Hash 能用来识别设备——边缘本来就在给每个请求记指纹,只是顺手贴一个到请求头里给源站用。

运营商这边,中国移动 4.43 万、中国电信 3.13 万、中国联通 1.63 万。三家都占着不小的量,就近接入对国内访客是有意义的。

QQ20261004-204734-1791118063183.webp

还有一条:中转站的 API 端点一周被调了 9506 次。这条链路上跑的是真实的 API 调用,不是挂在那里没人碰的静态站。

和上个月比一项

可缓存命中率从 8 月的 66.3% 升到这次的 76.9%。只放这一项对照——8 月那篇统计的是 20 天,这次是 7 天,窗口长度不一样,混在一起比容易看歪。

一周 28.7 万次请求里,这套配置真正管住的是那 2.48 万次。份额不到十分之一,但全站只有它这一条,每一次都得先问清楚来的是谁。

五、为什么我建议你也这么配

这条域名只占全站流量的 8.6%。为这么小的份额做一整套配置值不值——这是我用了两个月之后最想先说清楚的一件事。判断只对跟我情况相似的人成立。

我的访客基本都在国内

中转站服务的是协会成员,我的博客读者也大多在国内。ESA 的节点在国内,请求就近接入,这条路对我这种站点更贴合。

这是我按自己的访客构成做的选择,不是一句放之四海的结论。你的访客在哪儿,答案就该跟着变。

边缘能力是产品里的开关,不是我自己拼出来的一套规则

不少 CDN 的做法是让你自己在边缘规则里拼头,ESA 把这件事收进了托管转换——三个开关,不用自己写规则。

这件事的价值不在省事。省下的那点配置时间,一次就赚回来了。差别在少一处会出错的地方。

自己拼头,要维护一段规则;自己盯着回源网段,要定期对一遍。每一处都是一个将来会烂掉的配置——它今天是对的,对方调度一改就未必还是,而且它不会报错,只会在某个你没留意的时刻悄悄不对。而这条域名一周有 2.48 万次请求,每一条都得先问清楚来的是谁——这些将来会忘掉的配置,少一处是一处。

门槛也交代清楚

这套东西不是勾两下就完事。

源站侧要改 nginx,把 real_ip_header 指向 ali-real-client-ip;改完还得确认应用读的确实是那个头——这类配置只影响 nginx 眼里的 $remote_addr,如果应用自己解析 XFF,那里是另一处要改的地方。两步都得自己动手,中间没有任何一步会替你检查。

加速是我接它时想要的东西,可信是接完之后多出来的东西。

回到那一列 IP

我后来又翻了一次那份审计日志。还是那台中转站,还是同一列客户端 IP。

同一列,现在写的是真实的访客地址,还能看出他们分别从哪个城市来。当初那一列全是 47.123.117.x 这样的机房地址,读不出任何关于「谁」的信息;现在这一列,读得出。

如果你也在源站前面挂了 CDN,可以先翻一眼日志的第一列。挑一条记录,拿自己当时的公网 IP 对一下——对不上,源站手里那份身份就是假的,你在它上面搭的那些规矩,跟着一起是假的。