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-city | CN + 一个地级市名 |
| 添加安全请求头 | Tls-Hash | 32 位 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 的请求——那些每次都得回源、本来就不可能命中的——根本不进这个分母。要是拿全部请求当分母,数字会难看很多,但那已经不是同一件事了。
一周的缓存状态分布是这样:
| 缓存状态 | 请求数 |
|---|---|
| DYNAMIC | 15.8 万 |
| NONE / UNKNOWN | 5 万 |
| HIT | 3.46 万 |
| BYPASS | 3.27 万 |
| MISS | 1.04 万 |
DYNAMIC 占了一半还多,这没什么奇怪:博客的 HTML、接口请求、登录跳转本来就该走动态。真正落在可缓存区间里的那部分,四分之三被边缘直接接走了。
按域名看,这堆流量都是谁的
这张表是这一节我最想给你看的:
| Host | 请求数 |
|---|---|
blog.liuhangyv.top | 7.75 万 |
api.liuhangyv.top | 2.48 万 |
gitea.liuhangyv.top | 1.43 万 |
liuhangyv.top | 1.2 万 |
www.liuhangyv.top | 1.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 对一下——对不上,源站手里那份身份就是假的,你在它上面搭的限流、白名单、审计,跟着一起是假的。
评论交流
欢迎留下你的想法