You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在记录日志时对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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.28 18:27:46