第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 拦截器实现流程
实现流程包含四个步骤:
- 实现Interceptor接口,在intercept()方法中获取原始Request。
- 遍历原始请求的所有Header,判断是否为敏感字段。
- 对敏感字段执行移除或脱敏操作,构造新的Header集合。
- 使用新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()
✅ 调试验证方法:
- 将日志拦截器放置在过滤拦截器之后(先过滤再日志)。
- 运行任意带Authorization的请求,观察日志输出。
- 预期输出中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的日志过滤,防止日志逃逸至文件或网络。
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安全防护的重要一环。