title: 源站该信谁:给 AI 中转站配上阿里云 ESA 的边缘能力
date: 2026-10-05
author: liuhangyv
category: 技术思考
tags:

  • 技术深潜
    excerpt: 给协会的 AI 中转站做安全收口时,我发现源站手里那列「客户端 IP」全是机房的地址。这篇讲我是怎么用阿里云 ESA 把这件事扳回来的:边缘注入的三个客户端伪造不了的请求头、一份 258 条的信任网段清单,以及把它们接进 nginx 的完整配置。

一、先有前提,再谈策略

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

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

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

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

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

这不是我哪一步配错了

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

请求先到边缘节点,再由边缘节点回源——回源就是从边缘节点回到源站去取内容。这条链路上,跟源站建立连接的是边缘节点的回源出口,不是发起请求的那个成员。所以 nginx 里的 $remote_addr,回答的是「谁跟我建的这条连接」,而不是「谁发的这个请求」。

任何 CDN 都这样。只要你在源站前面加一层,源站看到的「客户端」就变成了那一层。跟选的是哪家无关,是链路结构决定的。

麻烦的地方在补不回来

如果只是记了个错值,换一种采集方式就能修。真正的问题是,这个值补不回来:回源 IP 和真实用户 IP 之间没有任何映射关系,信息在回源那一跳就丢了。

整个过程不报错,不告警,控制台一切正常,日志照常一行行写进去。要不是我拿着一条记录去核对,这件事不会被谁发现。

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

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

所以我要做的不是「再加一条规则」,而是先把源站手里的那份身份信息变回真的。

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

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

三个开关,勾一次就在了

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

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

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

源站收到的是哪几个头

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

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

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

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

怎么确认的?带上伪造值发一次请求,再看源站收到了什么:

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

这个测法不复杂,但它问的是最直接的那个问题:客户端能不能决定源站最终看到的值。答案是不能。

经 ESA 进来的请求,这几个头客户端改不了。这个结论的适用范围是「请求确实从 ESA 过」——这是所有基于「可信代理」的机制共同的前提。

差别不在名字,在信任是怎么建立的

X-Forwarded-For 是 HTTP 里的事实标准,任何客户端都能自己塞一个进去。HTTP 头本来就可以随便追加,也没有签名字段能证明某一条出自谁手。源站敢信 XFF,靠的从来不是这个头本身,而是额外的一条配置:我只信我自己的代理网段,别处写的一律不认。名字里带个 X-,不等于它比别的东西更可信。

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}'

带时间戳是为了让这次请求在日志里唯一可定位;awk '{print $1}' 取的是第一列,也就是 nginx 眼里的 $remote_addr。打出来是你自己的公网 IP,才算真的对上。

这里有个小坑:必须用 grep -a。access.log 里含二进制字节,普通 grep 会把它判成二进制文件,只甩一句 binary file matches 就拒绝输出,本来查得到的结果反而看不见。

为什么三个开关我都开着

只开第一个也够用,后两个是顺手勾的:地理位置头给审计多一个维度,不用自己接 IP 库;JA3 指纹可以在不看请求内容的前提下认设备,对风控是个现成的补充信号。都是产品里现成的开关,多开两个,不多一处要维护的配置。

到这一步,源站手里的那几个头,才第一次可以当成事实来用。

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

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

真实客户端 IP:那三件事自动成立

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

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

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

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

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

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

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

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

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

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

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

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

ali-real-client-ip 答「谁发的」,ali-ip-country / ali-ip-city 答「从哪来」,Tls-Hash 答「是不是同一侧的老客户端」——三个头各答一个具体问题。

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

先把口径交代清楚

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

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

缓存那一半,先看口径

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 万

博客第一,意料之中。中转站 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 万。国内三大运营商都有真实访问,就近接入对我的访客是有意义的。

路径里还有一项值得单独点出来:/v1/messages 一周 9506 次。这是中转站的核心 API 端点,说明这条链路上跑的是真实的 API 调用,不是挂在那里没人碰的静态站。

和上个月比一项

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

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

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

下面这些是我用了两个月之后的判断,只对跟我情况相似的人成立。

我的访客基本都在国内

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

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

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

不少 CDN 的做法是让你自己在边缘规则里拼头,ESA 是把这几个头做成了托管转换里的开关,选一下就有。

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

自己拼头,要维护一段规则;自己盯着回源网段,要定期对一遍。每一处都是一个将来会烂掉的配置——它今天是对的,对方调度一改就未必还是,而且它不会报错,只会在某个你没留意的时刻悄悄不对。这几处交给产品本身,我就不用一直惦记着。

门槛也交代清楚

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

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

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

回到那一列 IP

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

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

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