当Apache作为反向代理并启用mod_cache缓存后端JSON接口时,一个容易被忽视的问题是:缓存的是后端返回的完整响应体。如果某些字段只应该对特定调用方可见,或者包含内部调试信息、时间戳、冗余嵌套结构,缓存会把它们原封不动地分发给所有命中缓存的客户端。本文要解决的正是如何在代理缓存架构下实现JSON字段的精准过滤,同时避免破坏缓存的一致性。

为什么不能在缓存之后直接做过滤
很多管理员会想到一个简单方案:在Apache的输出链上添加mod_substitute对JSON响应做正则替换。例如后端返回{"username":"alice","ssn":"123-45-6789","role":"admin"},我们想移除ssn字段。如果在代理转发给客户端之前做一次s/"ssn":"[^"]*",?//替换,确实可以隐藏敏感信息。但问题在于,这个替换发生在缓存命中之后,也就是缓存中存储的仍然是包含ssn的完整JSON。一旦攻击者能够绕过替换规则,比如通过条件请求、不同的Accept头或分块传输方式访问同一个缓存条目,就可能拿到未过滤的原始数据。
更严重的是,如果使用了多个不同的过滤规则,所有客户端共享同一个缓存键,替换后的响应无法被正确地重新缓存或区分。例如A客户端需要过滤掉ssn,B客户端需要保留ssn但过滤掉role,两者请求同一个URL时,mod_cache只会命中同一个缓存条目,输出替换逻辑会相互干扰。因此,正确做法是在缓存写入之前完成过滤,让缓存中只存储已经清洗过的JSON。这样无论后续哪个客户端命中缓存,拿到的都是过滤后的结果,缓存条目也不再包含敏感字段。
要把过滤逻辑放到缓存写入之前,需要理解mod_cache的过滤链。在Apache 2.4中,mod_cache作为快速处理器和输出过滤器运行。输出内容的修改可以通过mod_substitute在缓存过滤器之前或之后介入。默认情况下,mod_substitute的过滤顺序可通过SubstituteInheritBefore以及模块加载顺序调整。为了让字段移除发生在磁盘缓存和内存缓存存储之前,我们需要确保mod_substitute的输出过滤器排在mod_cache的cache_save过滤器前面。
配置代理缓存与JSON字段过滤的完整步骤
以下配置假定Apache已经加载了mod_proxy、mod_proxy_http、mod_cache、mod_cache_disk、mod_substitute和mod_headers。我们以代理http://backend.internal/api/users为例,缓存只对GET请求生效,并且只处理内容类型为application/json的响应。
首先定义缓存目录并启用磁盘缓存。然后使用SetEnvIf和Header指令控制缓存键,确保带不同查询参数的请求不会互相污染。接下来用ProxyPass和ProxyPassReverse建立反向代理。在全局或VirtualHost级别使用CacheEnable disk /api/开启缓存。关键在于添加Substitute指令完成字段过滤,并用SubstituteMaxLineLength调大单行处理长度,避免长JSON被截断。
<VirtualHost *:80>
ServerName proxy.ippipp.com
# 缓存存储目录
CacheRoot /var/cache/apache2/mod_cache_disk
CacheEnable disk /api/
CacheDirLevels 2
CacheDirLength 1
CacheDefaultExpire 3600
CacheMaxExpire 86400
CacheIgnoreNoLastMod On
CacheHeader on
CacheDetailHeader on
# 只缓存GET请求
CacheIgnoreHeaders Set-Cookie
RequestHeader unset Cookie
SetEnvIf Request_Method "GET" CACHE_REQUEST
CacheEnable disk /api/
# 后端代理
ProxyPass /api/ http://backend.internal/api/
ProxyPassReverse /api/ http://backend.internal/api/
# 只对JSON响应启用替换
SetEnvIfNoCase Content-Type "application/json" DO_JSON_FILTER=1
# 使用mod_substitute在缓存写入前过滤字段
<Location /api/>
Substitute "s/\"ssn\":\"[^\"]*\",?//i" q
Substitute "s/\"internal_debug\":\{[^}]*\},?//i" q
SubstituteMaxLineLength 10m
AddOutputFilterByType SUBSTITUTE application/json
</Location>
# 强制内容类型保持为JSON
Header always set Content-Type "application/json" "expr=%{CONTENT_TYPE} =~ m#application/json#"
ErrorLog /var/log/apache2/proxy_error.log
CustomLog /var/log/apache2/proxy_access.log combined
</VirtualHost>
上面配置中的AddOutputFilterByType SUBSTITUTE application/json确保mod_substitute只对JSON响应生效。替换规则里的q标志表示静默替换,不写入替换日志。第一个正则移除ssn字符串字段,包括其后的逗号;第二个正则移除internal_debug对象字段。注意正则中的双引号在apache配置文件中不需要转义,但反斜杠需要正确书写。由于配置指令中使用了双引号界定正则,因此正则内部的单引号和普通双引号可以共存,但建议把正则放在双引号内并确保内部没有未转义的双引号破坏指令结构。
还需要注意mod_substitute与mod_cache的过滤顺序。在Apache 2.4的默认模块顺序下,SUBSTITUTE过滤器通常排在CACHE_SAVE之前,因此替换后的内容会进入缓存。为了确认这一点,可以在配置文件中显式查看mod_substitute的过滤链位置,或者在测试时先关闭缓存,验证替换生效后再开启缓存,观察缓存目录中的.response文件是否已经不再包含ssn字段。如果缓存目录中仍然存在未过滤的内容,说明替换发生在缓存之后,需要调整模块加载顺序,例如在mod_cache之前加载mod_substitute,或者使用FilterDeclare手动构建过滤链。
过滤规则的边界与缓存键设计
JSON字段过滤并非简单的字符串替换就能解决所有情况。字段值可能包含转义引号、嵌套对象、数组结构,或者字段名大小写不一致。对于复杂的过滤需求,正则表达式会变得极其脆弱。例如后端返回{"user":{"ssn":"123","name":"alice"}},单纯移除"ssn":"123"会遗留多余的逗号,导致JSON格式错误。针对嵌套对象,可以使用更精确的正则来匹配整个键值对,但前提是必须知道该字段值的类型。对于布尔型或数字型字段,正则模式需要区分引号是否存在。
一个更稳妥的做法是结合mod_lua或mod_python编写输出过滤器,用JSON解析库对响应体做结构化操作。不过本文聚焦于纯配置方案,因为它在大部分简单场景下足够可靠,且无需额外安装解析模块。如果你需要频繁处理复杂JSON结构,建议在Apache前面增加一层轻量级网关,或者让后端提供参数化的字段裁剪能力。但在无法改动后端的情况下,使用mod_substitute配合严格的字段正则仍然是最快捷的代理层过滤方式。
缓存键的设计直接影响过滤后的缓存命中率。如果所有客户端都请求同一个URL但期望不同的字段集合,那么一个共享缓存条目无法满足所有人。此时需要把客户端要求的字段列表纳入缓存键的一部分。可以通过CacheKeyBaseURL、CacheKeyQueryString或自定义请求头来构建不同的缓存键。例如客户端请求/api/users?fields=username,role时,Apache可以缓存两个不同键:一个是全字段版本,一个是过滤后版本。如果过滤规则对所有客户端一致,那么无需修改缓存键,所有客户端共享同一个已过滤的缓存条目即可。当过滤规则依赖请求参数时,则需要将参数加入缓存键,防止缓存错配。
最后要强调的是,过滤后的响应头应该保持与原始响应一致,特别是Content-Length必须更新为过滤后的实际长度,否则客户端会挂起或读取到错误的数据。mod_substitute会自动调整Content-Length,但在使用Header指令手动设置时不要覆盖这个行为。另外,如果响应经过gzip压缩,mod_substitute默认不会修改压缩内容,需要先用SetOutputFilter INFLATE解压处理再重新压缩,这会增加CPU开销。对于JSON接口,建议在代理层关闭后端压缩,或者使用mod_deflate在过滤后进行统一压缩,保持缓存内容为明文以便后续替换。
Apache代理缓存JSON字段过滤mod_proxy修改时间:2026-08-27 21:18:52