Protobuf可选字段接收多值时能否开启严格模式报错?
问题描述
搭建了一个gRPC服务端,接收的消息包含optional字段。由于客户端运行在不可信环境,可能收到无效或篡改的消息。测试发现,当客户端向该可选字段发送多个值(类似repeated字段的行为)时,Protobuf会忽略多余值,仅保留最后一个,而非抛出异常。
使用版本:com.google.protobuf:protobuf-java:3.23.3,测试代码如下:
message Msg1 { repeated string field = 1; } message Msg2 { optional string field = 1; } Msg1 msg1 = Msg1.newBuilder().addField("value1").addField("value2").addField("value3").build(); byte[] data = msg1.toByteArray(); // 客户端序列化 Msg2 msg2 = Msg2.parseFrom(data); // 服务端反序列化,希望格式不匹配时失败 System.out.println(msg2.getField()); // 输出 "value3"
解决方案
Protobuf Java 3.x没有内置的"严格模式"来检测重复的非repeated字段,默认行为是保留最后一个值,这是符合Protobuf规范的向后兼容设计。要实现检测或抛出异常,需要自定义解析逻辑,以下是两种可行方案:
方案1:自定义严格解析器
继承Protobuf的AbstractParser,在解析过程中统计每个字段的出现次数,当非repeated字段重复出现时抛出异常:
import com.google.protobuf.AbstractParser; import com.google.protobuf.CodedInputStream; import com.google.protobuf.Descriptors; import com.google.protobuf.ExtensionRegistryLite; import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.Message; import com.google.protobuf.WireFormat; import java.util.HashMap; import java.util.Map; public class StrictParser<T extends Message> extends AbstractParser<T> { private final AbstractParser<T> delegate; private final Descriptors.Descriptor descriptor; public StrictParser(AbstractParser<T> delegate, Descriptors.Descriptor descriptor) { this.delegate = delegate; this.descriptor = descriptor; } @Override public T parseFrom(CodedInputStream input, ExtensionRegistryLite extensionRegistry) throws IOException { Map<Integer, Integer> fieldCount = new HashMap<>(); // 包装输入流,统计每个字段的出现次数 T message = delegate.parseFrom(new ForwardingCodedInputStream(input) { @Override public int readTag() throws IOException { int tag = super.readTag(); if (tag != 0) { int fieldNumber = WireFormat.getTagFieldNumber(tag); fieldCount.put(fieldNumber, fieldCount.getOrDefault(fieldNumber, 0) + 1); } return tag; } }, extensionRegistry); // 检查非repeated字段的重复情况 for (Descriptors.FieldDescriptor field : descriptor.getFields()) { if (!field.isRepeated()) { int count = fieldCount.getOrDefault(field.getNumber(), 0); if (count > 1) { throw new InvalidProtocolBufferException( String.format("字段 %s (编号 %d) 重复出现 %d 次,但该字段并非repeated类型", field.getName(), field.getNumber(), count)); } } } return message; } // 实现ForwardingCodedInputStream,转发大部分方法到原始输入流 private static class ForwardingCodedInputStream extends CodedInputStream { private final CodedInputStream delegate; public ForwardingCodedInputStream(CodedInputStream delegate) { this.delegate = delegate; } @Override public int readTag() throws IOException { return delegate.readTag(); } @Override public void skipField(int tag) throws IOException { delegate.skipField(tag); } @Override public int readRawVarint32() throws IOException { return delegate.readRawVarint32(); } @Override public int readRawVarint64() throws IOException { return delegate.readRawVarint64(); } @Override public void readRawBytes(byte[] value) throws IOException { delegate.readRawBytes(value); } // 按需重写其他需要的方法,确保解析正常进行 } }
使用方式:
StrictParser<Msg2> strictParser = new StrictParser<>(Msg2.parser(), Msg2.getDescriptor()); try { Msg2 msg2 = strictParser.parseFrom(data); } catch (InvalidProtocolBufferException e) { // 处理重复字段异常,比如拒绝请求 e.printStackTrace(); }
方案2:手动逐字段解析检查
直接使用CodedInputStream逐字段解析,当发现非repeated字段已被设置时抛出异常:
import com.google.protobuf.CodedInputStream; import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.WireFormat; Msg2.Builder builder = Msg2.newBuilder(); CodedInputStream input = CodedInputStream.newInstance(data); int tag; try { while ((tag = input.readTag()) != 0) { int fieldNumber = WireFormat.getTagFieldNumber(tag); if (fieldNumber == Msg2.FIELD_FIELD_NUMBER) { if (builder.hasField()) { throw new InvalidProtocolBufferException("字段'field'重复出现"); } builder.mergeFieldFrom(tag, input); } else { // 可选:处理未知字段,这里直接跳过,也可以选择抛出异常 input.skipField(tag); } } Msg2 msg2 = builder.build(); } catch (InvalidProtocolBufferException e) { // 异常处理逻辑 e.printStackTrace(); }
说明
Protobuf的默认行为是为了向后兼容:旧版本的客户端可能会重复发送非repeated字段,新版本服务端需要兼容这种情况。如果你的场景必须严格校验消息格式,上述两种方案都可以实现需求。
内容的提问来源于stack exchange,提问作者JMax
相关产品推荐
相关产品推荐

