赞
踩
前段时间网上爆出FastJson漏洞后就接到公司安全部门硬性通知的全司升级,要求所有的fastjson升级到1.2.73版本,并且在发布系统进行jar包版本拦截,低于这个版本不让发布。这招比较狠!于是开启了升级之路。大家都想到了升级可能会有风险,都比较忐忑,但事还得做。
幸运的是升级了好几个服务都顺利升级了,还挺开心的,觉得不用担心了。剩下最后一个老服务升级了。这天和其他服务升级一样也没有太担心,正常上线了一台机器。本来不想看日志检测是否有问题的,过了会觉得还是看看比较放心点。打开日志系统后发现有几条ERROR日志报’Content-Type’ can not contain wildcard type ‘*’。
[2020-12-16 14:13:58.266] [http-nio-8092-exec-9] [ERROR] org.apache.catalina.core.ContainerBase.[Tomcat].[localhost].[/].[dispatcherServlet]:181 x:() - Servlet.service() for servlet [dispatcherServlet]
in context with path [] threw exception [Request processing failed; nested exception is java.lang.IllegalArgumentException: 'Content-Type' cannot contain wildcard type '*'] with root cause
java.lang.IllegalArgumentException: 'Content-Type' cannot contain wildcard type '*'
心里想可能以前也有报这个错误,搜索没有发布的机器上的日志,没有发现这个错误。惊呆了,升级还是踩坑了,赶紧拉下线。还好虽然有少量报错,业务还没有感知,幸亏去检查了,发现及时没有吃个线上事件。
因为是Get请求,拷贝了同样的请求放在浏览器里请求了报错的机器,发现接口正常返回。看了下http请求的头文件如下:
难道业务方的代码里header指定的Accept 特殊?可是通过线上的日志已经nginx的日志里查看到请求的Header信息。
先看了下代码,这个接口是Get请求,RequestMapping注解里没有指定produces和consumes的值也没有打@ResponseBody的注解。
要解决问题就得在本地复现问题。令我失望的是不管用浏览器直接调用,还是postman指定Header的Accept的各种值都是正常返回结果。用同样的方式在测试环境尝试也都是正常的,没有报错。连复现都不行,难道线上的报错是偶然导致的?那找测试人员在预发布用postman试试,令我意想不到的是还真复现了。看来上线前测试人员在预发布没有常规测试啊,如果常规测试肯定是可以发现这个问题的。于是去测试人员那里看了下请求的Header,发现postman默认的Accept是*/*。fastjson版本回退后在预发布重新发布,同样的请求就可以正常返回了,但是以同样的方式和高版本的fastjson在测试环境和本地测试,结果是正常的。这就令人费解了。在本地不能复现要去debug就比较难了。
还是先看看源码分析下吧。
应用配做的HttpMessageConverter只配置了StringHttpMessageConverter 和FastJsonHttpMessageConverter
@Configuration
public class FastJsonHttpMessageConverterConfig {
/**
添加JSON序列化方式,默认是Jackson,替换为FastJson
@return:HttpMessageConverters
*/
@Bean
public HttpMessageConverters injectFastJsonHttpMessageConverter(){
FastJsonHttpMessageConverter fastJsonHttpMessageConverter=new FastJsonHttpMessageConverter();
FastJsonConfig fastJsonConfig=new FastJsonConfig();
fastJsonConfig.setSerializerFeatures(SerializerFeature.PrettyFormat);
fastJsonHttpMessageConverter.setFastJsonConfig(fastJsonConfig);
StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter();
stringHttpMessageConverter.setDefaultCharset(StandardCharsets.UTF_8);
List converters=new ArrayList();
converters.add(stringHttpMessageConverter);
converters.add(fastJsonHttpMessageConverter);
return new HttpMessageConverters(false,converters);
}
}
org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor#writeWithMessageConverters()方法,具体逻辑可以参考代码注解
protected <T> void writeWithMessageConverters(T value, MethodParameter returnType,
ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage)
throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
省略部分代码//
HttpServletRequest request = inputMessage.getServletRequest();
//从request的Header里获取Accept的值,可以参考
//org.springframework.web.accept.HeaderContentNegotiationStrategy#resolveMediaTypes
//前面已经分析过了,对应我的情况Header的Accept是*/*,所以requestedMediaTypes是MediaType.ALL
List<MediaType> requestedMediaTypes = getAcceptableMediaTypes(request);
//获取支持的MediaTypes
//1、先从request的attribute里获取@RequestMapping注解的produces配置的值,如果配置了就返回配置的MediaTypes,否则转第2步
//2、从所有配置的HttpMessageConverter里匹配canWrite该返回值的Converter,并获取对应Converter SupportedMediaTypes的值
//应用的实际情况分析开始/
//对应我的情况@RequestMapping没有配置produces,那么应该取所有HttpMessageConverter的SupportedMediaTypes。
//根据AbstractHttpMessageConverter的getSupportedMediaTypes源码可以知道supportedMediaTypes的值是在HttpMessageConverter实例化通过构造方法参数传入的
//FastJsonHttpMessageConverter是MediaType.ALL
//StringHttpMessageConverter是MediaType.TEXT_PLAIN, MediaType.ALL
//而这两个Converter能canWrite对象的就只有FastJsonHttpMessageConverter
//所以producibleMediaTypes就只有MediaType.ALL也就是*/*
应用的实际情况分析结束/
List<MediaType> producibleMediaTypes = getProducibleMediaTypes(request, valueType, declaredType);
省略部分代码//
Set<MediaType> compatibleMediaTypes = new LinkedHashSet<MediaType>();
//从ProducibleMediaTypes里选择可以兼容requestedType的MediaType的类型
for (MediaType requestedType : requestedMediaTypes) {
for (MediaType producibleType : producibleMediaTypes) {
if (requestedType.isCompatibleWith(producibleType)) {
compatibleMediaTypes.add(getMostSpecificMediaType(requestedType, producibleType));
}
}
}
省略部分代码//
List<MediaType> mediaTypes = new ArrayList<MediaType>(compatibleMediaTypes);
//对可以支持requestedType即Header的Accept的所有MediaType进行排序
// */*的MediaType会排在其他类型的后面
MediaType.sortBySpecificityAndQuality(mediaTypes);
MediaType selectedMediaType = null;
for (MediaType mediaType : mediaTypes) {
//从排序后的MediaTypes里选择一个正确的MediaType
//根据isConcrete代码逻辑是非*/*的MediaType就是正确的
if (mediaType.isConcrete()) {
//如果MediaType不是*/*则返回该MediaType
selectedMediaType = mediaType;
break;
}
//如果该MediaType是MediaType.ALL或者MEDIA_TYPE_APPLICATION,则selectedMediaType=MediaType.APPLICATION_OCTET_STREAM
//应用的实际情况分析开始/
//从上面的分析看我的应用的实际情况是mediaType只要MediaType.ALL
//所以这里selectedMediaType会被赋值为MediaType.APPLICATION_OCTET_STREAM
///应用的实际情况分析结束///
else if (mediaType.equals(MediaType.ALL) || mediaType.equals(MEDIA_TYPE_APPLICATION)) {
selectedMediaType = MediaType.APPLICATION_OCTET_STREAM;
break;
}
}
//如果selectedMediaType!=null,则从所有配置的HttpMessageConverter里选择一个可以支持selectedMediaType类型的Converter,并调用对应的write方法
if (selectedMediaType != null) {
selectedMediaType = selectedMediaType.removeQualityValue();
for (HttpMessageConverter<?> messageConverter : this.messageConverters) {
if (messageConverter instanceof GenericHttpMessageConverter) {
//省略部分代码
//并调用对应的write方法并传入selectedMediaType
//应用的实际情况分析开始/
//selectedMediaType=MediaType.APPLICATION_OCTET_STREAM
///应用的实际情况分析结束///
((GenericHttpMessageConverter) messageConverter).write(
outputValue, declaredType, selectedMediaType, outputMessage);
//省略部分代码
}
下面是fastjson的1.2.73FastJsonHttpMessageConverter的源码,可以看到调用了super.write()方法。
com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter.write()
/**
* Can serialize/deserialize all types.
*/
public FastJsonHttpMessageConverter() {
super(MediaType.ALL);
}
public void write(Object o, Type type, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
//应用的实际情况分析开始/
//contentType=MediaType.APPLICATION_OCTET_STREAM
///应用的实际情况分析结束///
super.write(o, contentType, outputMessage);// support StreamingHttpOutputMessage in spring4.0+
//writeInternal(o, outputMessage);
}
org.springframework.http.converter.AbstractHttpMessageConverter#write
方法里调用了addDefaultHeaders方法
public final void write(final T t, MediaType contentType, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
//应用的实际情况分析开始/
//contentType=MediaType.APPLICATION_OCTET_STREAM
///应用的实际情况分析结束///
final HttpHeaders headers = outputMessage.getHeaders();
addDefaultHeaders(headers, t, contentType);
//省略部分代码///
}
else {
writeInternal(t, outputMessage);
outputMessage.getBody().flush();
}
}
///addDefaultHeaders方法
protected void addDefaultHeaders(HttpHeaders headers, T t, MediaType contentType) throws IOException{
//如果Header的ContentType==null
//应用的实际情况分析开始/
//contentType=MediaType.APPLICATION_OCTET_STREAM
//Header的Content-Type也是null
///应用的实际情况分析结束///
if (headers.getContentType() == null) {
MediaType contentTypeToUse = contentType;
/**
*如果传入的contentType==null或者则contentType是 */*
*则从默认支持的MediaType里选择第一个MediaType
*/
if (contentType == null || contentType.isWildcardType() || contentType.isWildcardSubtype()) {
contentTypeToUse = getDefaultContentType(t);
}
/**
*如果传入的contentType是application/octet-stream
*则从默认支持的MediaType里选择第一个MediaType
*如果默认的选出的MediaType==null就使用传入的contentType
*/
//应用的实际情况分析开始/
//contentType=MediaType.APPLICATION_OCTET_STREAM
//这个时候就会调用getDefaultContentType而FastJsonConvert的default值就是MediaType.ALL,即contentTypeToUse=MediaType.ALL
///应用的实际情况分析结束///
else if (MediaType.APPLICATION_OCTET_STREAM.equals(contentType)) {
MediaType mediaType = getDefaultContentType(t);
contentTypeToUse = (mediaType != null ? mediaType : contentTypeToUse);
}
if (contentTypeToUse != null) {
if (contentTypeToUse.getCharset() == null) {
Charset defaultCharset = getDefaultCharset();
if (defaultCharset != null) {
contentTypeToUse = new MediaType(contentTypeToUse, defaultCharset);
}
}
//设置Header的ContentType为contentTypeToUse
//应用的实际情况分析开始/
//contentTypeToUse=MediaType.ALL
///应用的实际情况分析结束
headers.setContentType(contentTypeToUse);
}
}
if (headers.getContentLength() < 0 && !headers.containsKey(HttpHeaders.TRANSFER_ENCODING)) {
Long contentLength = getContentLength(t, headers.getContentType());
if (contentLength != null) {
headers.setContentLength(contentLength);
}
}
}
org.springframework.http.converter.AbstractHttpMessageConverter#getDefaultContentType
private List<MediaType> supportedMediaTypes = Collections.emptyList();
protected MediaType getDefaultContentType(T t) throws IOException {
List<MediaType> mediaTypes = getSupportedMediaTypes();
return (!mediaTypes.isEmpty() ? mediaTypes.get(0) : null);
}
//这里的supportedMediaTypes属性是在创建HttpMessageConverter实例时通过构造方法传入的,
//而FastJsonHttpMessageConverter的构造方法是传的MediaType.ALL而MediaType.ALL对应的就是 "*/*"
@Override
public List<MediaType> getSupportedMediaTypes() {
return Collections.unmodifiableList(this.supportedMediaTypes);
}
org.springframework.http.HttpHeaders#setContentType
这里就是报’Content-Type’ cannot contain wildcard type '*'错误的源头了。
/**
* Set the {@linkplain MediaType media type} of the body,
* as specified by the {@code Content-Type} header.
*/
public void setContentType(MediaType mediaType) {
//如果mediaType是*/*则报错
//应用的实际情况分析开始/
//传入的mediaType=MediaType.ALL,所以报错
///应用的实际情况分析结束
Assert.isTrue(!mediaType.isWildcardType(), "'Content-Type' cannot contain wildcard type '*'");
Assert.isTrue(!mediaType.isWildcardSubtype(), "'Content-Type' cannot contain wildcard subtype '*'");
set(CONTENT_TYPE, mediaType.toString());
}
从上面的源码分析到新版的FastJsonHttpMessageConverter的构造方法是传的MediaType.ALL而MediaType.ALL对应的就是 "*/*"难道是新版本这里修改了,于是看了老版本的,结果让人失望。跟现在的版本一样的。
哪为什么升级前的没有报错呢??
再在看看老版本的write方法。下面是1.2.33版本FastJsonHttpMessageConverter的write方法
/*
* @see org.springframework.http.converter.GenericHttpMessageConverter#write(java.lang.Object, java.lang.reflect.Type, org.springframework.http.MediaType, org.springframework.http.HttpOutputMessage)
*/
public void write(Object t, //
Type type, //
MediaType contentType, //
HttpOutputMessage outputMessage //
) throws IOException, HttpMessageNotWritableException {
HttpHeaders headers = outputMessage.getHeaders();
if (headers.getContentType() == null) {
if (contentType == null || contentType.isWildcardType() || contentType.isWildcardSubtype()) {
contentType = getDefaultContentType(t);
}
if (contentType != null) {
headers.setContentType(contentType);
}
}
if (headers.getContentLength() == -1) {
Long contentLength = getContentLength(t, headers.getContentType());
if (contentLength != null) {
headers.setContentLength(contentLength);
}
}
writeInternal(t, outputMessage);
outputMessage.getBody().flush();
}
这个方法1.2.73版的不一样,1.2.73版的是直接调用了AbstractHttpMessageConverter#write方法,而1.2.33版本是完全重写了父类的方法自己实现的。但粗略的看和AbstractHttpMessageConverter#write前面的逻辑一样也是判断ContentType然后设置headers.setContentType(contentType),但是细看就会发现秘密。1.2.33版本重写的地方是如果传入的contentType!=null 就直接设置,而通过前面的分析这时的contentType=MediaType.APPLICATION_OCTET_STREAM,所以headers.setContentType(contentType)(这个方法是报错的地方)的时候不是*/*,不会报错,而AbstractHttpMessageConverter#write方法的addDefaultHeaders方法里是增加了如下代码,当contentType=MediaType.APPLICATION_OCTET_STREAM是获取了FastJsonHttpMessageConverter的默认的MediaType.ALL
else if (MediaType.APPLICATION_OCTET_STREAM.equals(contentType)) {
MediaType mediaType = getDefaultContentType(t);
contentTypeToUse = (mediaType != null ? mediaType : contentTypeToUse);
}
if (contentTypeToUse != null) {
if (contentTypeToUse.getCharset() == null) {
Charset defaultCharset = getDefaultCharset();
if (defaultCharset != null) {
contentTypeToUse = new MediaType(contentTypeToUse, defaultCharset);
}
}
通过上面的一顿分析终于找到了升级FastJson报错的真正原因,找到原因了就好解决了,但是还有个问题在困扰这我。那就是前面说了本地很测试环境为什么不报错??
通过字节码插桩技术,在代码了加入了日志通过本地与预发布的的日志对比,发现本地环境的messageConverters比线上的多了一个MappingJackson2HttpMessageConverter。而MappingJackson2HttpMessageConverter支持的MediaType是MediaType.APPLICATION_JSON, new MediaType(“application”, “*+json”),所以在org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor#getProducibleMediaTypes()的时候获取到的producibleMediaTypes就是
而不只是*/*,排序后
所以后面的设置header就不会报错。
通过Debug发现在应用启动是有个BeanPostProcessor springfox.documentation.spring.web.ObjectMapperConfigurer在会postProcessBeforeInitialization里会加入MappingJackson2HttpMessageConverter
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof RequestMappingHandlerAdapter) {
RequestMappingHandlerAdapter adapter = (RequestMappingHandlerAdapter) bean;
adapter.setMessageConverters(configureMessageConverters(adapter.getMessageConverters()));
}
return bean;
}
private List<HttpMessageConverter<?>> configureMessageConverters(List<HttpMessageConverter<?>> converters) {
Iterable<MappingJackson2HttpMessageConverter> jackson2Converters = jackson2Converters(converters);
if (Iterables.size(jackson2Converters) > 0) {
for (MappingJackson2HttpMessageConverter each : jackson2Converters) {
fireObjectMapperConfiguredEvent(each.getObjectMapper());
}
} else {
converters.add(configuredMessageConverter());
}
return newArrayList(converters);
}
那为什么只有本地和测试环境会有添加这个MappingJackson2HttpMessageConverter呢?难道哪里有什么配置?
通过查看ObjectMapperConfigurer的包名发现是来自于springfox-spring-web,通过查看包依赖发现这个包是属于springfox-swagger2引入的。
突然就豁然开朗了,我在配置swagger时指定了适用于"dev",“test”,
@Profile({"dev","test"})
@Configuration
@EnableSwagger2
public class Swagger2 {}
profile 是dev或者test时@EnableSwagger2注解被激活,同时@Import({Swagger2DocumentationConfiguration.class})就被激活
@Retention(value = java.lang.annotation.RetentionPolicy.RUNTIME)
@Target(value = { java.lang.annotation.ElementType.TYPE })
@Documented
@Import({Swagger2DocumentationConfiguration.class})
public @interface EnableSwagger2 {
}
@Import({Swagger2DocumentationConfiguration.class})被激活时SpringfoxWebMvcConfiguration.class被激活
@Configuration
@Import({ SpringfoxWebMvcConfiguration.class, SwaggerCommonConfiguration.class })
@ComponentScan(basePackages = {
"springfox.documentation.swagger2.readers.parameter",
"springfox.documentation.swagger2.web",
"springfox.documentation.swagger2.mappers"
})
public class Swagger2DocumentationConfiguration {
@Bean
public JacksonModuleRegistrar swagger2Module() {
return new Swagger2JacksonModule();
}
}
SpringfoxWebMvcConfiguration.class被激活时就会注入BeanPostProcessor的ObjectMapperConfigurer实例
@EnableAspectJAutoProxy
public class SpringfoxWebMvcConfiguration {
@Bean
public Defaults defaults() {
return new Defaults();
}
@Bean
public DocumentationCache resourceGroupCache() {
return new DocumentationCache();
}
@Bean
public static ObjectMapperConfigurer objectMapperConfigurer() {
return new ObjectMapperConfigurer();
}
@Bean
public JsonSerializer jsonSerializer(List<JacksonModuleRegistrar> moduleRegistrars) {
return new JsonSerializer(moduleRegistrars);
}
}
终于所有的疑惑都清楚了。
可以在@RequestMapping注解里加入produces = “application/json;charset=UTF-8”
在FastJsonHttpMessageConverter设置支持的MediaType替换默认的MediaType.ALL
List<MediaType> supportMediaTypeList = new ArrayList<>();
supportMediaTypeList.add(MediaType.TEXT_HTML);
supportMediaTypeList.add(MediaType.APPLICATION_JSON);
supportMediaTypeList.add(MediaType.APPLICATION_JSON_UTF8);
supportMediaTypeList.add(MediaType.valueOf("application/*+json"));
supportMediaTypeList.add(MediaType.ALL);
fastJsonHttpMessageConverter.setSupportedMediaTypes(supportMediaTypeList);
但是鉴于已经上线的接口,如果在@RequestMapping注解里加入produces 可能会影响到业务调用,如果有业务指定了Accept是text\html就报错了,当然在RequestMapping的produces也可以指定多个MediaType,但是如果还有别的接口也这样的,那就需要修改多个地方,最终还是选择了方案2。上线后确实也解决了报错的问题。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。