由前端空字符串到springboot后端变null的问题的追踪(converter与Formatter)

文章来源原创   作者:临窗旋墨   发布时间:2022-09-19   阅读:1153   标签:spring,事务,springboot 分类:springmvc 专题:我读[不懂]源码

由前端空字符串到springboot后端变null的问题的追踪

环境springboot 2.7.3

关键词HandlerMethodArgumentResolverCompositeFormattingConversionServiceGenericConversionService

1 问题描述

最近小伙伴们发现了一个问题, 就是前端参数传的空字符串,到后端变成了null,一时摸不到头脑,让我帮一起看看问题所在。

2 问题猜测

  1. 是否通过@InitBinder 注册了registerCustomEditor对String类型的参数做了转换

    • 然而并没有
  2. 是否是Filter过滤器中对参数做了再次处理

    • 比如前端对参数转json后加密,后端通过HttpServletRequestWrapper解密重写参数,然后也不是
  3. 是否自定义了Converter(implements org.springframework.core.convert.converter.Converter.Converter<String, String>) 对空字符串做了特殊转换,

    • 然后还是没有
  4. 是否自定义了Formatter(implements org.springframework.format.Formatter.Formatter<String>)对字符串进行了转换

    • 这个真的有:

      1. public class XssStringFormatter implements Formatter<String> {
      2. @Override
      3. public String parse(String text, Locale locale) throws ParseException {
      4. return text;
      5. }
      6. @Override
      7. public String print(String object, Locale locale) {
      8. return StringEscapeUtils.escapeHtml4(object) ;
      9. }
      10. }
    • 但是代码如上,只是在输出的时候进行了转义,防止XSS攻击,而在parse方法中并没有做任何操作

3 只好看一下源码了

我猜测是在参数转换那里出了问题,因此把断点打在HandlerMethodArgumentResolverCompositeresolveArgument方法上。

  • HandlerMethodArgumentResolverComposite#resolveArgument
  • RequestParamMethodArgumentResolverAbstractNamedValueMethodArgumentResolver)#resolveArgument

  • DataBinder#convertIfNecessary

  • TypeConverterSupport#convertIfNecessary
  • TypeConverterDelegate#convertIfNecessary
  • GenericConversionService#convert
  1. public Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType) {
  2. Assert.notNull(targetType, "Target type to convert to cannot be null");
  3. if (sourceType == null) {
  4. Assert.isTrue(source == null, "Source must be [null] if source type == [null]");
  5. return handleResult(null, targetType, convertNullSource(null, targetType));
  6. }
  7. if (source != null && !sourceType.getObjectType().isInstance(source)) {
  8. throw new IllegalArgumentException("Source to convert from must be an instance of [" +
  9. sourceType + "]; instead it was a [" + source.getClass().getName() + "]");
  10. }
  11. //此处获取到的是FormattingConversionService,且conversionService属性中的
  12. //converterCache包含的为String → XssStringFormatter
  13. GenericConverter converter = getConverter(sourceType, targetType);
  14. if (converter != null) {
  15. Object result = ConversionUtils.invokeConverter(converter, source, sourceType, targetType);
  16. return handleResult(sourceType, targetType, result);
  17. }
  18. return handleConverterNotFound(source, sourceType, targetType);
  19. }
  • 通过ConversionUtils.invokeConverter(converter, source, sourceType, targetType);进入到的方法为:

    • FormattingConversionService$ParserConverter#convert,在这个方法中如果参数为空字符串,则直接返回null
    1. public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
    2. String text = (String) source;
    3. //如果参数为空字符串,则直接返回null
    4. if (!StringUtils.hasText(text)) {
    5. return null;
    6. }
    7. Object result;
    8. try {
    9. result = this.parser.parse(text, LocaleContextHolder.getLocale());
    10. }
    11. catch (IllegalArgumentException ex) {
    12. throw ex;
    13. }
    14. catch (Throwable ex) {
    15. throw new IllegalArgumentException("Parse attempt failed for value [" + text + "]", ex);
    16. }
    17. TypeDescriptor resultType = TypeDescriptor.valueOf(result.getClass());
    18. if (!resultType.isAssignableTo(targetType)) {
    19. result = this.conversionService.convert(result, resultType, targetType);
    20. }
    21. return result;
    22. }

但是当不在项目中注册自定义的Formatter的时候,ConversionUtils.invokeConverter(converter, source, sourceType, targetType);进入的方法却是GenericConversionServiceconvert方法,最重要的区别就是在这里了。

​ 至于为什么会使用不同的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

  1. /**
  2. * 描述: 写入前端的json字段做xss处理
  3. * @author Vic.xu
  4. */
  5. public class XssStringJsonSerializer extends JsonSerializer<String> {
  6. @Override
  7. public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
  8. if (value != null) {
  9. String encodedValue = StringEscapeUtils.escapeHtml4(value);
  10. gen.writeString(encodedValue);
  11. }
  12. }
  13. }

2 注入ObjectMapper

  1. @Bean
  2. @Primary
  3. public ObjectMapper objectMapper(Jackson2ObjectMapperBuilder build) {
  4. logger.info("register xssObjectMapper");
  5. ObjectMapper objectMapper = build.createXmlMapper(false).build();
  6. SimpleModule simpleModule = new SimpleModule(XssStringJsonSerializer.class.getSimpleName());
  7. simpleModule.addSerializer(String.class, new XssStringJsonSerializer());
  8. objectMapper.registerModule(simpleModule);
  9. objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
  10. return objectMapper;
  11. }

3 对于不需要进行转义的字段,则在字段上加IgnoreXssStringJsonSerializer 注解

  1. /**
  2. * 写入前端的字段忽略xss处理 {@link XssStringJsonSerializer}
  3. * @author Vic.xu
  4. */
  5. public class IgnoreXssStringJsonSerializer extends JsonSerializer<String> {
  6. @Override
  7. public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
  8. if (value != null) {
  9. gen.writeString(value);
  10. }
  11. }
  12. }

参考文档:

  1. 6. 抹平差异,统一类型转换服务ConversionService

  2. springboot 全局转换器和参数校验

  3. 细节见真章,Formatter注册中心的设计很讨巧

2022-09


发表评论

目录