由前端空字符串到springboot后端变null的问题的追踪
环境:springboot 2.7.3
关键词: HandlerMethodArgumentResolverComposite、FormattingConversionService、GenericConversionService
1 问题描述
最近小伙伴们发现了一个问题, 就是前端参数传的空字符串,到后端变成了null,一时摸不到头脑,让我帮一起看看问题所在。
2 问题猜测
-
是否通过
@InitBinder注册了registerCustomEditor对String类型的参数做了转换- 然而并没有
-
是否是
Filter过滤器中对参数做了再次处理- 比如前端对参数转
json后加密,后端通过HttpServletRequestWrapper解密重写参数,然后也不是
- 比如前端对参数转
-
是否自定义了
Converter(implementsorg.springframework.core.convert.converter.Converter.Converter<String, String>) 对空字符串做了特殊转换,- 然后还是没有
-
是否自定义了
Formatter(implementsorg.springframework.format.Formatter.Formatter)对字符串进行了转换 -
这个真的有:
javapublic class XssStringFormatter implements Formatter<String> { @Override public String parse(String text, Locale locale) throws ParseException { return text; } @Override public String print(String object, Locale locale) { return StringEscapeUtils.escapeHtml4(object) ; } } -
但是代码如上,只是在输出的时候进行了转义,防止XSS攻击,而在parse方法中并没有做任何操作
-
3 只好看一下源码了
我猜测是在参数转换那里出了问题,因此把断点打在HandlerMethodArgumentResolverComposite的resolveArgument方法上。
-
HandlerMethodArgumentResolverComposite#resolveArgument→ -
RequestParamMethodArgumentResolver(AbstractNamedValueMethodArgumentResolver)#resolveArgument→ -
DataBinder#convertIfNecessary→ -
TypeConverterSupport#convertIfNecessary→ -
TypeConverterDelegate#convertIfNecessary→ -
GenericConversionService#convert
public Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType) {
Assert.notNull(targetType, "Target type to convert to cannot be null");
if (sourceType == null) {
Assert.isTrue(source == null, "Source must be [null] if source type == [null]");
return handleResult(null, targetType, convertNullSource(null, targetType));
}
if (source != null && !sourceType.getObjectType().isInstance(source)) {
throw new IllegalArgumentException("Source to convert from must be an instance of [" +
sourceType + "]; instead it was a [" + source.getClass().getName() + "]");
}
//此处获取到的是FormattingConversionService,且conversionService属性中的
//converterCache包含的为String → XssStringFormatter
GenericConverter converter = getConverter(sourceType, targetType);
if (converter != null) {
Object result = ConversionUtils.invokeConverter(converter, source, sourceType, targetType);
return handleResult(sourceType, targetType, result);
}
return handleConverterNotFound(source, sourceType, targetType);
}
-
通过
ConversionUtils.invokeConverter(converter, source, sourceType, targetType);进入到的方法为:FormattingConversionService$ParserConverter#convert,在这个方法中如果参数为空字符串,则直接返回null
javapublic Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { String text = (String) source; //如果参数为空字符串,则直接返回null if (!StringUtils.hasText(text)) { return null; } Object result; try { result = this.parser.parse(text, LocaleContextHolder.getLocale()); } catch (IllegalArgumentException ex) { throw ex; } catch (Throwable ex) { throw new IllegalArgumentException("Parse attempt failed for value [" + text + "]", ex); } TypeDescriptor resultType = TypeDescriptor.valueOf(result.getClass()); if (!resultType.isAssignableTo(targetType)) { result = this.conversionService.convert(result, resultType, targetType); } return result; }
但是当不在项目中注册自定义的Formatter的时候,ConversionUtils.invokeConverter(converter, source, sourceType, targetType);进入的方法却是GenericConversionService的convert方法,最重要的区别就是在这里了。
至于为什么会使用不同的convert,就在 GenericConversionService#getConverter(TypeDescriptor sourceType, TypeDescriptor targetType)中,本文就不展开赘述, 可以参考问候的参考文档第一篇。
4 解决方式:ObjectMapper注册一个Xss解析器
项目中的XssStringFormatter本意是字符串数据输入到前端的时候,通过StringEscapeUtils.escapeHtml4(object)进行html转义,一定程度上预防XSS攻击 。
项目使用的为springboot + thymeleaf, 向页面传参主要通过两种方式一个是thymeleaf模板中直接使用变量,而th:text是默认转义的。另外就是ajax返回的json数据了,在注入的ObjectMappeing中注册一个Xss解析器即可:
1 定义 XssStringJsonSerializer
java
/**
* 描述: 写入前端的json字段做xss处理
* @author Vic.xu
*/
public class XssStringJsonSerializer extends JsonSerializer<String> {
@Override
public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
if (value != null) {
String encodedValue = StringEscapeUtils.escapeHtml4(value);
gen.writeString(encodedValue);
}
}
}
2 注入ObjectMapper
java
@Bean
@Primary
public ObjectMapper objectMapper(Jackson2ObjectMapperBuilder build) {
logger.info("register xssObjectMapper");
ObjectMapper objectMapper = build.createXmlMapper(false).build();
SimpleModule simpleModule = new SimpleModule(XssStringJsonSerializer.class.getSimpleName());
simpleModule.addSerializer(String.class, new XssStringJsonSerializer());
objectMapper.registerModule(simpleModule);
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
return objectMapper;
}
3 对于不需要进行转义的字段,则在字段上加IgnoreXssStringJsonSerializer 注解
/**
* 写入前端的字段忽略xss处理 {@link XssStringJsonSerializer}
* @author Vic.xu
*/
public class IgnoreXssStringJsonSerializer extends JsonSerializer<String> {
@Override
public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
if (value != null) {
gen.writeString(value);
}
}
}
参考文档:
2022-09
GitHub 登录
Gitee 登录
QQ 登录