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

原创 临窗旋墨 2022-09-19 阅读:5068 分类: java后端 专题: 源码阅读 标签: springmvc,spring,源码

由前端空字符串到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)对字符串进行了转换

    • 这个真的有:

      java 复制代码
      public 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 只好看一下源码了

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

  • HandlerMethodArgumentResolverComposite#resolveArgument

  • RequestParamMethodArgumentResolverAbstractNamedValueMethodArgumentResolver)#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
    java 复制代码
    public 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);进入的方法却是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

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);
       }
   }
}

参考文档:

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

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

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

2022-09

评论区

avatar
未登录

暂无评论,来发表第一条评论吧~