公司使用我定制过的swagger作为接口文档平台。昨日同事反映一个问题,说mvc控制器中新增加了一个接口,写法与其他接口无异,为什么加上他swagger接口文档平台就报错、注释掉他即正常?
正好最近由于fastjson的反序列化绕过黑名单机制RCE漏洞事件,正研究fastjson及其他json序列化工具的反序列化安全问题,对这方面比较敏感。
确认同事的描述无误,发现浏览器f12看到的错误原因是json解析异常。继而检查入参和返回的dto,发现在其中一个字段的example中填写了[2020/01/01, 2020/01/03]这样的值。
中括号显然是json的保留字符,果然去掉中括号、或者在值前后加上双引号,都可以解决问题。
但是事情并未结束,在我的认知中,swagger对example并未有特殊的说明,各种json序列化工具也不会擅自对String类型值进行判断输出推断后的结果,在引发操作错误的风险下费力的做类型推断和转换的脏活累活,swagger是出于什么考虑呢?
首先回顾一下基本知识,swagger原理在这篇文章中有着很周到的叙述,简要提两点,就不再赘述了:
1、利用spring plugin机制收集documention属性、扫描handlers apis、存入cache
2、前端从cache获取数据进行展示
详见https://blog.csdn.net/qq_25615395/java/article/details/70229139
从swagger扫描机制入手,对其进行跟进,发现其直至存入document缓存(之后json序列化输出供前端调用),对应model的example值始终为String类型。
那么剩余的流程只有json序列化,swagger使用jackson(ObjectMapper)作为json序列化工具。
不难发现模型example属性的特殊json处理源于Swagger2JacksonModule(这里应该也允许我们自定义自己的Module定制序列化过程),
Swagger2JacksonModule对swagger序列化文档模型输出时使用的ObjectMapper进行了注册。
@Override
public void setupModule(SetupContext context) {
super.setupModule(context);
...
context.setMixInAnnotations(Property.class, PropertyExampleSerializerMixin.class);
}
@JsonAutoDetect
@JsonInclude(value = Include.NON_EMPTY)
private interface PropertyExampleSerializerMixin {
@JsonSerialize(using = PropertyExampleSerializer.class)
Object getExample();
class PropertyExampleSerializer extends StdSerializer<Object> {
private final static Pattern JSON_NUMBER_PATTERN =
Pattern.compile("-?(?:0|[1-9]\\d*)(?:\\.\\d+)?(?:[eE][+-]?\\d+)?");
@SuppressWarnings("unused")
public PropertyExampleSerializer() {
this(Object.class);
}
PropertyExampleSerializer(Class<Object> t) {
super(t);
}
@Override
public void serialize(Object value, JsonGenerator gen, SerializerProvider provider) throws IOException {
if (canConvertToString(value)) {
String stringValue = (value instanceof String) ? ((String) value).trim() : value.toString().trim();
if (isStringLiteral(stringValue)) {
String cleanedUp = stringValue.replaceAll("^\"", "")
.replaceAll("\"$", "")
.replaceAll("^‘", "")
.replaceAll("‘$", "");
gen.writeString(cleanedUp);
} else if (isNotJsonString(stringValue)) {
gen.writeRawValue(stringValue);
} else {
gen.writeString(stringValue);
}
} else {
gen.writeObject(value);
}
}
private boolean canConvertToString(Object value) {
if (value instanceof java.lang.Boolean
|| value instanceof java.lang.Character
|| value instanceof java.lang.String
|| value instanceof java.lang.Byte
|| value instanceof java.lang.Short
|| value instanceof java.lang.Integer
|| value instanceof java.lang.Long
|| value instanceof java.lang.Float
|| value instanceof java.lang.Double
|| value instanceof java.lang.Void) {
return true;
}
return false;
}
@VisibleForTesting
boolean isStringLiteral(String value) {
return (value.startsWith("\"") && value.endsWith("\""))
|| (value.startsWith("‘") && value.endsWith("‘"));
}
@VisibleForTesting
boolean isNotJsonString(final String value) {
// strictly speaking, should also test for equals("null") since {"example": null} would be valid JSON
// but swagger2 does not support null values
// and an example value of "null" probably does not make much sense anyway
return value.startsWith("{") // object
|| value.startsWith("[") // array
|| "true".equals(value) // true
|| "false".equals(value) // false
|| JSON_NUMBER_PATTERN.matcher(value).matches(); // number
}
@Override
public boolean isEmpty(SerializerProvider provider, Object value) {
return internalIsEmpty(value);
}
@SuppressWarnings("deprecation")
@Override
public boolean isEmpty(Object value) {
return internalIsEmpty(value);
}
private boolean internalIsEmpty(Object value) {
return value == null || value.toString().trim().length() == 0;
}
}
}
可以看出若example属性不是基本数据类型的包装类或字符串,则按照对象序列化;继续判断是否以双引号/单引号开头结尾,如是按字符串序列化;继续判断是否以大括号/中括号开头,如是按对象/列表序列化,此处应是为了支持json字符串格式的example属性赋值,然而遇到手写并不规范的example值时就造成了输出的json格式无法解析的错误。
至此,问题告一段落。
/**
* Updates the Example for the model
*
* @param example - example of the model
* @return this
* @deprecated @since 2.8.1 Use the one which takes in an Object instead
*/
@Deprecated
public ModelBuilder example(String example) {
this.example = defaultIfAbsent(example, this.example);
return this;
}
从源码的注释中可以看出自2.8.1版本以来,swagger将扫描api过程中接收example属性的类型字段由String改为Object,
然而注解的成员变量必须是一个编译期常量,example属性如何接受一个object?swagger好像也并未告诉我们。