4. HTTP请求与响应中的数据脱敏:URL参数、请求头、请求体、Cookie
说实话,在接口安全里,数据脱敏最难的不是算法本身,而是“在哪脱、什么时候脱”。
我遇到过不少团队,脱敏逻辑写得挺漂亮,但选错了拦截点——要么在日志输出前脱,结果上游链路已经泄露了;要么在响应体里脱了,URL参数里还挂着明文。所以这一章,咱们把HTTP请求和响应的每个“窟窿”都捋一遍,看看URL参数、请求头、请求体、Cookie分别该怎么处理。
4.1 脱敏的“四个口袋”
一次完整的HTTP交互,敏感数据可能藏在四个地方:
- URL参数:GET/POST的查询字符串、RESTful路径变量
- 请求头:Authorization、X-Forwarded-For、自定义令牌头
- 请求体:JSON/XML包体里的身份证、手机号、银行卡
- Cookie:SessionID、登录态令牌、用户标识
你想想看,要是一个接口把用户手机号放在URL参数里,后端日志一录,中间件一转发,可能就到处是明文了。
核心原则:数据脱敏必须在进入业务逻辑之前完成,并且要对请求和响应双向覆盖。
4.2 URL参数脱敏
我记得有一次做渗透测试,发现一个电商接口的订单详情GET请求是 /order/123456?phone=13800138000。这不等于把用户手机号写在墙上吗?
URL参数脱敏分两种场景:
4.2.1 查询字符串脱敏
对于GET请求的 ?key=value,建议在网关层或API网关的过滤器里统一处理。
// Java伪代码:在Filter中替换URL参数
String maskPhone(String phone) {
if (phone == null || phone.length() < 7) return phone;
return phone.substring(0, 3) + "****" + phone.substring(7);
}
// 使用方式:从HttpServletRequest的getQueryString()中解析
String query = request.getQueryString();
// 正则替换 phone=13800138000 → phone=138****8000
避坑指南:我见过有人直接修改 request.getParameterMap(),但那是不可变的。正确做法是重写 HttpServletRequestWrapper,覆盖 getParameter() 等方法。
4.2.2 RESTful路径变量脱敏
比如 /users/{userId}/orders/{orderId},如果userId本身就是手机号,那路径里就是明文。我曾经在项目里做过一个规则:路径变量中包含手机号格式(11位数字/1xx开头)的,自动用 **** 替换中间四位。
注意:不要在网关层面直接对路径变量做硬脱敏,否则下游路由可能失败。建议在请求进入业务层之前,把脱敏后的参数存入一个独立的ThreadLocal上下文,供日志使用。
4.3 请求头脱敏
请求头是“暗流”。很多人只盯着报文体,忽略了头部。
需要重点关注的请求头:
Authorization:Bearer 、Basic认证的Base64密码X-Forwarded-For:客户端IP(可能涉及隐私)Cookie:见4.5节- 自定义头:比如
X-User-Mobile、X-Real-Name
我个人习惯在网关层写一个统一的HeaderMaskFilter,把所有敏感头脱敏后再传给下游。
// 脱敏Authorization头
String maskAuth(String auth) {
if (auth == null) return null;
if (auth.startsWith("Bearer ")) {
String = auth.substring(7);
return "Bearer " + .substring(0, 6) + "****";
}
return "****";
}
为什么只保留前6位? 我调试问题时需要知道这个是哪个环境/用户签发的,完全脱敏会导致排障困难。保留前缀,既安全又可用。
4.4 请求体/响应体脱敏
这是主战场。JSON/XML里的敏感字段最多。
4.4.1 基于注解的脱敏(推荐)
在Java里用Jackson的 @JsonSerialize 配合自定义序列化器,可以轻松实现。我做过一个 @Sensitive 注解,标记在字段上就自动脱敏。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@JacksonAnnotationsInside
@JsonSerialize(using = SensitiveSerializer.class)
public @interface Sensitive {
SensitiveType type() default SensitiveType.MOBILE;
}
// 使用
public class UserResponse {
@Sensitive(type = SensitiveType.MOBILE)
private String phone;
@Sensitive(type = SensitiveType.ID_CARD)
private String idCard;
}
注意:这种注解方案只对序列化生效(响应体)。如果要对请求体(反序列化)脱敏,则需要额外写 @JsonDeserialize 或在Filter里使用中间对象替换。
4.4.2 基于规则的通用脱敏
如果无法修改POJO(例如第三方SDK的请求体),可以用正则或递归遍历JSON树。我在项目中用过一个简单的递归脱敏器:
// 递归遍历JSON树,将指定字段名的值用正则替换
public static JsonNode maskJson(JsonNode node, Set<String> fields) {
if (node.isObject()) {
ObjectNode obj = (ObjectNode) node;
fields.forEach(field -> {
if (obj.has(field)) {
String original = obj.get(field).asText();
obj.put(field, maskByType(original));
}
});
// 递归子节点
obj.fields().forEachRemaining(entry ->
obj.set(entry.getKey(), maskJson(entry.getValue(), fields)));
}
return node;
}
性能警告:大规模JSON递归可能导致CPU飙升。建议对响应体开启内容压缩(gzip)+ 缓存脱敏结果,或者使用Streaming解析。
4.5 Cookie脱敏
Cookie是浏览器侧的“存根”,很多系统直接把SessionID明文存进去。更危险的是,有些API在响应头里 Set-Cookie 直接返回了用户手机号。
我做过的一个C端项目,上线后发现 Set-Cookie: userId=13800138000……那是真正的“裸奔”。
处理建议:
- 请求Cookie:在网关层重写
Cookie请求头,把敏感值替换为HMAC摘要 - 响应Set-Cookie:在响应过滤器里拦截,对值做脱敏(比如截断、替换中间位)
- 日志脱敏:无论请求还是响应,记录日志前必须将Cookie中的敏感字段脱敏
// 响应Set-Cookie脱敏示例(Spring Filter)
String setCookie = response.getHeader("Set-Cookie");
if (setCookie != null && setCookie.contains("phone=")) {
// 正则替换 phone=13800138000 → phone=138****8000
setCookie = setCookie.replaceAll("phone=\\d{11}", "phone=138****8000");
response.setHeader("Set-Cookie", setCookie);
}
个人做法:我一般不直接在Cookie里存手机号/身份证。如果非要存,就存一个唯一的掩码ID(如 user_hash=abc123),在服务端通过缓存映射回真实信息。这样即使Cookie泄露,攻击者也拿不到明文。
4.6 总结:一张图看清脱敏覆盖
好了,四种位置、两种方向(请求/响应)、三种脱敏时机(序列化时、过滤器、日志输出)。我把它们的关系整理成一张结构图:
这张图想说的是:脱敏不是只做一边。请求进来要脱,响应出去也要脱。而且每一类数据源都可能需要不同的脱敏策略。
4.7 避坑指南汇总
| 位置 | 常见坑 | 我的建议 |
|---|---|---|
| URL参数 | 直接在网关修改参数值导致路由失败 | 用Wrapper模式,不修改原始URL,只修改getParameter()的返回值 |
| 请求头 | Authorization头脱敏后下游无法鉴权 | 在日志记录时脱敏,不影响鉴权流程 |
| 请求体 | 递归遍历JSON导致性能问题 | 优先使用注解方案(零性能开销),必要时开启gzip |
| Cookie | Set-Cookie里明文存敏感信息 | 改用哈希+缓存映射,或直接不存敏感值 |
| 日志 | 只脱敏了响应体,没脱敏请求的URL参数 | 统一日志拦截器,对Request+Response全量脱敏 |
重要提醒:不要认为“我的API只对内网调用”就不做脱敏。内网日志被运维看到、被测试工程师不小心导出、被容器stdout收集到,这些场景我都遇见过。安全是一层一层的,每层都做,最后才可能不漏。