如何在记录日志时对gRPC响应的指定字段进行脱敏处理
嘿,这个需求太常见了!很多项目里都需要对敏感字段做日志脱敏,而且得保证发给客户端的响应不受影响,我给你分享几个实际项目里验证过的靠谱方案:
几种可行的实现方案
方法一:通过拦截器/过滤器捕获响应后脱敏
如果你的项目是基于Spring这类框架的,用拦截器是最直接的方式——它能在响应已经发送给客户端之后,拿到响应内容进行脱敏再打印日志。
核心思路是:
- 在请求进入时,用
ContentCachingResponseWrapper包装原始响应,这样后续可以重复读取响应体; - 在请求完全处理完成(响应已发给客户端)的钩子方法里,读取缓存的响应体,对敏感字段(比如
phoneNumber)做脱敏; - 打印脱敏后的日志,最后别忘了把缓存的响应内容写回原始响应(避免客户端拿不到数据)。
举个Spring拦截器的示例代码:
public class ResponseDesensitizationInterceptor implements HandlerInterceptor { private static final Logger logger = LoggerFactory.getLogger(ResponseDesensitizationInterceptor.class); private final ObjectMapper objectMapper = new ObjectMapper(); @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 包装响应,缓存响应体以便后续读取 if (!(response instanceof ContentCachingResponseWrapper)) { request.setAttribute("cachedResponse", new ContentCachingResponseWrapper(response)); return true; } return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { ContentCachingResponseWrapper cachedResponse = (ContentCachingResponseWrapper) request.getAttribute("cachedResponse"); if (cachedResponse == null) { return; } // 读取响应体 byte[] responseBytes = cachedResponse.getContentAsByteArray(); String responseContent = new String(responseBytes, cachedResponse.getCharacterEncoding()); // 脱敏处理敏感字段 try { JsonNode rootNode = objectMapper.readTree(responseContent); if (rootNode.has("phoneNumber")) { String originalPhone = rootNode.get("phoneNumber").asText(); String desensitizedPhone = DesensitizationUtils.desensitizePhone(originalPhone); ((ObjectNode) rootNode).put("phoneNumber", desensitizedPhone); responseContent = objectMapper.writeValueAsString(rootNode); } // 打印脱敏后的日志 logger.info("Desensitized response log: {}", responseContent); } catch (JsonProcessingException e) { logger.error("Failed to desensitize response", e); // 解析失败时打印原始日志(避免丢失信息) logger.info("Original response log: {}", responseContent); } finally { // 将缓存的响应内容写回,确保客户端能正常接收 cachedResponse.copyBodyToResponse(); } } }
方法二:用AOP切面环绕日志打印方法
如果你的项目是通过特定的工具类方法打印响应日志(比如LogUtil.logResponse(Object response)),那用AOP环绕这个方法会更灵活——在日志打印前对响应对象做脱敏,不影响业务逻辑。
核心步骤:
- 定义一个切面,环绕你的日志打印方法;
- 在切面逻辑中,拿到要打印的响应对象,修改敏感字段;
- 将脱敏后的对象传给日志方法执行打印。
示例代码(Spring AOP):
@Aspect @Component public class ResponseLogDesensitizationAspect { @Around("execution(* com.yourcompany.LogUtil.logResponse(..))") public Object aroundLogResponse(ProceedingJoinPoint joinPoint) throws Throwable { Object[] args = joinPoint.getArgs(); if (args.length > 0 && args[0] instanceof YourResponseClass) { YourResponseClass response = (YourResponseClass) args[0]; // 脱敏手机号(注意:如果不想修改原对象,可以拷贝一个新对象再脱敏) response.setPhoneNumber(DesensitizationUtils.desensitizePhone(response.getPhoneNumber())); args[0] = response; } // 执行原始日志打印方法 return joinPoint.proceed(args); } }
方法三:自定义JSON序列化器(针对JSON日志场景)
如果你的日志是JSON格式输出(比如用Logback+Jackson),可以给敏感字段自定义序列化器,只在日志序列化时触发脱敏逻辑,完全不影响发给客户端的响应序列化。
核心思路:
- 写一个自定义序列化器,处理
phoneNumber字段的脱敏; - 为日志打印专门创建一个
ObjectMapper,注册这个自定义序列化器; - 打印日志时用这个专用的
ObjectMapper序列化响应对象。
示例序列化器代码:
public class PhoneDesensitizationSerializer extends StdSerializer<String> { public PhoneDesensitizationSerializer() { super(String.class); } @Override public void serialize(String phone, JsonGenerator gen, SerializerProvider provider) throws IOException { if (phone == null || phone.length() != 8) { gen.writeString(phone); return; } // 保留前2位,后面用******代替 gen.writeString(phone.substring(0, 2) + "******"); } }
然后在日志工具类里使用专用的ObjectMapper:
public class LogUtil { private static final ObjectMapper LOG_MAPPER = new ObjectMapper(); private static final Logger logger = LoggerFactory.getLogger(LogUtil.class); static { // 注册自定义序列化器 SimpleModule module = new SimpleModule(); module.addSerializer(String.class, new PhoneDesensitizationSerializer()); LOG_MAPPER.registerModule(module); } public static void logResponse(Object response) { try { String jsonStr = LOG_MAPPER.writeValueAsString(response); logger.info("Response log: {}", jsonStr); } catch (JsonProcessingException e) { logger.error("Failed to serialize response for log", e); } } }
通用脱敏工具类(必配)
不管用哪种方案,都需要一个通用的脱敏工具类来处理具体的字段规则,比如:
public class DesensitizationUtils { // 手机号脱敏(示例:8位号码保留前2位) public static String desensitizePhone(String phone) { if (phone == null || phone.isBlank()) { return phone; } // 根据实际手机号长度调整规则,比如11位手机号可以用 phone.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2") if (phone.length() == 8) { return phone.substring(0, 2) + "******"; } return phone; } // 可以扩展其他敏感字段的脱敏方法,比如身份证、邮箱等 public static String desensitizeIdCard(String idCard) { if (idCard == null || idCard.length() != 18) { return idCard; } return idCard.substring(0, 4) + "************" + idCard.substring(16); } }
注意事项
- 确保脱敏逻辑不会修改发给客户端的响应:比如用拦截器时一定要写回缓存的原始响应,用AOP时如果不想影响业务对象,可以拷贝一个新对象再脱敏;
- 处理异常情况:比如响应体解析失败、字段不存在时,要降级打印原始日志,避免丢失关键信息;
- 不同字段的脱敏规则要明确:比如手机号、身份证、邮箱的脱敏格式可能不一样,要根据业务需求调整。
内容的提问来源于stack exchange,提问作者Really Joey
相关产品推荐
相关产品推荐

