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 总结:一张图看清脱敏覆盖

好了,四种位置、两种方向(请求/响应)、三种脱敏时机(序列化时、过滤器、日志输出)。我把它们的关系整理成一张结构图:

HTTP请求与响应数据脱敏覆盖图 请求 (Request) URL 参数 请求头 请求体 Cookie 脱敏处理器 响应 (Response) 响应体 Set-Cookie 响应头 脱敏后的日志 / 存储 / 展示 双向脱敏:请求和响应 都需要处理 脱敏时机:网关/过滤器 或序列化阶段

这张图想说的是:脱敏不是只做一边。请求进来要脱,响应出去也要脱。而且每一类数据源都可能需要不同的脱敏策略。

4.7 避坑指南汇总

位置 常见坑 我的建议
URL参数 直接在网关修改参数值导致路由失败 用Wrapper模式,不修改原始URL,只修改getParameter()的返回值
请求头 Authorization头脱敏后下游无法鉴权 在日志记录时脱敏,不影响鉴权流程
请求体 递归遍历JSON导致性能问题 优先使用注解方案(零性能开销),必要时开启gzip
Cookie Set-Cookie里明文存敏感信息 改用哈希+缓存映射,或直接不存敏感值
日志 只脱敏了响应体,没脱敏请求的URL参数 统一日志拦截器,对Request+Response全量脱敏

重要提醒:不要认为“我的API只对内网调用”就不做脱敏。内网日志被运维看到、被测试工程师不小心导出、被容器stdout收集到,这些场景我都遇见过。安全是一层一层的,每层都做,最后才可能不漏。