公司使用我定制過的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好像也並未告訴我們。