第4章 请求头隐私过滤:移除或脱敏Authorization、Cookie、X-等敏感Header

请求头是网络传输中携带元数据的核心载体,但同时也是隐私泄漏的高发区。Authorization、Cookie、X-等头字段常直接暴露用户的认证凭据或会话标识,一旦被输出到日志、监控面板或第三方拦截器,可能导致凭证被窃取、会话被劫持。本章从原理、实现流程、代码、调试、部署及异常处理六个维度,讲解如何使用OkHttp拦截器对请求头进行过滤与脱敏。

4.1 敏感Header的分类与泄露风险

根据实际业务场景,敏感Header可分为三类:

类别典型字段泄露影响
认证凭据Authorization、X-、攻击者可直接使用该凭证调用受保护API
会话标识Cookie(如JSESSIONID)、Set-Cookie会话固定攻击或会话劫持
内部标识X-Forwarded-For、X-Real-IP、自定义TraceId暴露内部网络拓扑或用户行为轨迹
⚠️ 常见误区:很多开发者认为HTTPS加密后请求头不会被截获,但拦截器中的日志输出、HttpLoggingInterceptor打印、第三方库回调等均可能在明文层访问原始请求头。必须主动过滤。

4.2 过滤策略:移除 vs 脱敏

两种策略适用场景不同:

  • 完全移除:用于下游模块不需要该Header的场景。如向日志记录器传递时抹掉Authorization。
  • 部分脱敏:保留格式但替换敏感内容。如将Bearer xxx替换为Bearer [REDACTED],便于排查认证流程是否到达。
// 脱敏示例:保留前缀,替换凭证体
fun redactAuthorization(original: String): String {
    return when {
        original.startsWith("Bearer ") -> "Bearer [REDACTED]"
        original.startsWith("Basic ") -> "Basic [REDACTED]"
        else -> original
    }
}

4.3 拦截器实现流程

实现流程包含四个步骤:

  1. 实现Interceptor接口,在intercept()方法中获取原始Request。
  2. 遍历原始请求的所有Header,判断是否为敏感字段。
  3. 对敏感字段执行移除或脱敏操作,构造新的Header集合。
  4. 使用新Header集合构建新的Request对象,传递给chain.proceed()。
💡 关键点:OkHttp的Request.Builder不支持直接修改已有Header,需移除原Header再添加新值。务必使用Header(name, value)方法移除所有同名Header后新增,避免遗留旧值。

4.4 完整代码实现

class PrivacyHeaderInterceptor(
    private val sensitiveHeaders: Set<String> = setOf("Authorization", "Cookie", "X-"),
    private val mode: FilterMode = FilterMode.REDACT
) : Interceptor {

    enum class FilterMode { REMOVE, REDACT }

    override fun intercept(chain: Interceptor.Chain): Response {
        val originalRequest = chain.request()
        val newHeaders = Headers.Builder()

        for (headerName in originalRequest.headers.names()) {
            val headerValue = originalRequest.headers[headerName] ?: continue
            if (headerName in sensitiveHeaders) {
                when (mode) {
                    FilterMode.REMOVE -> {
                        // 跳过添加,即移除
                    }
                    FilterMode.REDACT -> {
                        val redactedValue = redactHeader(headerName, headerValue)
                        newHeaders.add(headerName, redactedValue)
                    }
                }
            } else {
                newHeaders.add(headerName, headerValue)
            }
        }

        val newRequest = originalRequest.newBuilder()
            .headers(newHeaders.build())
            .build()

        return chain.proceed(newRequest)
    }

    private fun redactHeader(name: String, value: String): String {
        return when (name) {
            "Authorization" -> {
                when {
                    value.startsWith("Bearer ") -> "Bearer [REDACTED]"
                    value.startsWith("Basic ") -> "Basic [REDACTED]"
                    else -> "[REDACTED]"
                }
            }
            "Cookie" -> {
                val parts = value.split(";")
                parts.joinToString("; ") { part ->
                    val eqIdx = part.indexOf('=')
                    if (eqIdx > 0) "${part.substring(0, eqIdx + 1)}[REDACTED]"
                    else "[REDACTED]"
                }
            }
            "X-" -> "[REDACTED]"
            else -> "[REDACTED]"
        }
    }
}

4.5 注册与调试

在OkHttpClient构建时添加拦截器:

val client = OkHttpClient.Builder()
    .addInterceptor(PrivacyHeaderInterceptor(mode = FilterMode.REDACT))
    .addInterceptor(HttpLoggingInterceptor().apply {
        level = HttpLoggingInterceptor.Level.HEADERS
    })
    .build()
✅ 调试验证方法:
  1. 将日志拦截器放置在过滤拦截器之后(先过滤再日志)。
  2. 运行任意带Authorization的请求,观察日志输出。
  3. 预期输出中Authorization应显示为Bearer [REDACTED],而非真实。

验证日志示例:

--> POST https://api.example.com/v1/users
Content-Type: application/json
Authorization: Bearer [REDACTED]
Cookie: session=[REDACTED]; path=/
X-: [REDACTED]
--> END POST

4.6 常见错误与稳定性优化

错误场景原因解决方案
非法Header(空值或特殊字符)某些HTTP客户端会在拦截器中构造空值Header添加null检查,跳过非ASCII字符
Cookie被截断多值Cookie格式复杂,简单按分号切分可能漏掉使用正则或Cookie解析库(如aCrawler)
性能下降每次请求都遍历所有Header使用sensHeaderSet的O(1)查找;考虑缓存构造后的Headers
过滤顺序错误日志拦截器在过滤拦截器之前执行确保隐私过滤拦截器位于日志拦截器之间(如日志-过滤-日志两层)
⚠️ 稳定性注意:修改后的Request必须保证所有必需Header(如Content-Length、Host)均未被误伤移除。建议在拦截器末尾添加断言:检查是否遗漏了Host等必要字段。

4.7 部署建议

  • 在application级别OkHttpClient中全局配置PrivacyHeaderInterceptor。
  • 对于不同区域(测试/生产),通过配置开关控制模式:生产环境使用REMOVE,测试环境使用REDACT。
  • 若使用Retrofit,确保OkHttpClient在创建Retrofit实例前配置。
  • 结合log4j或Timber的日志过滤,防止日志逃逸至文件或网络。
请求头隐私过滤拦截器工作流程 原始 HTTP 请求 含敏感Header 遍历所有Header 判断是否在敏感集 移除模式 跳过添加该Header 脱敏模式 替换值为 [REDACTED] 构建新Request chain.proceed() 💡 说明: 任何敏感Header都不会传递给下一个拦截器或日志,真实凭证仅在内存中存在。 建议在PrivacyHeaderInterceptor之后添加日志拦截器,确保日志无泄漏。

4.8 性能考量

由于每次请求都需要构造新的Headers对象,频繁创建对象可能增加GC压力。优化方向:

  • 使用预编译的敏感Header集合(Set.of()),保证contains操作O(1)。
  • 对大部分请求Header不变的情况,考虑缓存脱敏后的Headers对象;但需注意Header值可能变化(如Cookie含时间戳)。
  • 在OkHttpClient级别添加连接级别的Header过滤,而非每次请求都复制整个Headers。
// 部分缓存示例(仅对完全可预知的静态Header生效)
private val cachedHeaders = ConcurrentHashMap<String, Headers>()

fun intercept(chain: Interceptor.Chain): Response {
    val request = chain.request()
    val cacheKey = request.url.encodedPath // 非权威缓存键,仅示例
    val newHeaders = cachedHeaders.getOrPut(cacheKey) {
        buildSafeHeaders(request.headers)
    }
    val newRequest = request.newBuilder().headers(newHeaders).build()
    return chain.proceed(newRequest)
}
💡 生产建议:多数项目直接使用无缓存版本即可满足性能需求。只有在每秒数千请求的高并发场景下,才考虑缓存机制。缓存时必须注意Header的动态性(如Cookie中的刷新)。

至此,你已经掌握了在OkHttp中通过自定义拦截器过滤敏感请求头的完整技术方案,包括移除与脱敏两种策略、调试验证方法、常见错误避免和性能优化方向。该方案可有效防止凭证泄露,是API安全防护的重要一环。