如何处理@PathVariable中的特殊字符问题
上代码:
@GetMapping(value="/user/{useraccount}") publicvoidgetUserAccount(@PathVariable("useraccount")StringuserAccount){ logger.info("useraccount:"+userAccount); }
正常访问:
/user/zhangsan
打印:useraccount:zhangsan
看似一切正常
but:
访问:/user/zhangsan/lisi
打印:useraccount:zhangsan
咦,为啥不是useraccount:zhangsan/lisi?
@PathVariable并没有我们想象的聪明,对于参数中的/并不能跟实际路径/分开
事实上,有.;-等都不能正确切分。
怎么办呢?
两种方案:
1,简单点,直接使用@RequestParam代替
@GetMapping(value="/user") publicvoidgetUserAccount(@RequestParam("useraccount")StringuserAccount){ logger.info("useraccount:"+userAccount); }
用/user?useraccount=zhangsan访问
2,使用正则过滤
@GetMapping(value="/user/{useraccount:[a-zA-Z0-9\\.\\-\\_\\;\\\]+}") publicvoidgetUserAccount(@PathVariable("useraccount")StringuserAccount){ logger.info("useraccount:"+userAccount); }
正常访问:
/user/zhangsan
打印:useraccount:zhangsan
当然,这个就有点不灵活了,第一种简单又方便
补充:记一次@PathVariable特殊参数会丢失的排查问题
请求参数中如果包含.,会造成参数丢失,请看如下代码
以下代码,省略@RestController控制层类代码
@RequestMapping(value="hello/{name}") publicMapsayHello(@PathVariable("name")Stringname,HttpServletRequestrequest){ Map rtnMap=newHashMap<>(); rtnMap.put("msg","hello"+name); returnrtnMap; }
请求地址:hello/ddf,则正常返回{"msg":"helloddf"}
请求地址:hello/ddf.com,依然还是返回{"msg":"helloddf"}
如果需要解决上面这个问题,则可以将代码更改如下(该解决方式从网上搜寻)
@RequestMapping(value="hello/{name:.*}") publicMapsayHello(@PathVariable("name")Stringname,HttpServletRequestrequest){ Map rtnMap=newHashMap<>(); rtnMap.put("msg","hello"+name); returnrtnMap; }
如果使用@PathVariable以.sh或.bat等特殊字符结尾,会影响实际返回数据
报错如下:
{ "timestamp":1541405292119, "status":406, "error":"NotAcceptable", "exception":"org.springframework.web.HttpMediaTypeNotAcceptableException", "message":"Couldnotfindacceptablerepresentation", "path":"/HDOrg/user/hello/ddf.sh" }
还是上面的代码
以下代码,省略@RestController控制层类代码
@RequestMapping(value="hello/{name:.*}") publicMapsayHello(@PathVariable("name")Stringname,HttpServletRequestrequest){ Map rtnMap=newHashMap<>(); rtnMap.put("msg","hello"+name); returnrtnMap; }
如果这时候请求地址为hello/ddf.sh或hello/ddf.com.sh,只要是以.sh结尾,这时候业务逻辑代码不会受到影响,但走到Spring自己的代码去处理返回数据的时候,有一个功能会根据扩展名来决定返回的类型,而以.sh结尾扩展名为sh,会被解析成对应的Content-Type:application/x-sh。
解决办法如下,第一种方法是从网上找到的,可以直接禁用该功能,但有可能会影响到静态资源的访问,不能确定,也没有进行尝试
@Configuration publicclassConfigextendsWebMvcConfigurerAdapter{ @Override publicvoidconfigureContentNegotiation( ContentNegotiationConfigurerconfigurer){ configurer.favorPathExtension(false); } }
然后以下就是闲着没事很想换个思路尝试去看看这到底是怎么回事,由于个人能力有限,不保证以下内容的重要性;
第二种方式解决思路是,既然扩展名以.sh等结尾会有问题,那么能不能不要让程序将扩展名识别为.sh,或者干脆就跳过处理,比如我是否可以加个.sh/这样就会影响到实际的扩展名,但是又不会影响到已有的代码,其实这里有个偷懒的写法,可以直接在@RequestMapping里的value最后直接加一个/,但是这要求客户端必须在原有的条件上最终拼一个/,否则会找不到对应的映射,直接404,我这里碰到这个问题的时候,因为该方法已经上线并且被其它几个系统调用,因此更改起来会有些繁琐,所以寻求了一种麻烦的方式,先将解决方式放在下面,不确定是否会影响其它问题
这种方式解决方式如下:注释中的两行代码二选一都可,推荐前面的写法,直接已经跳过
@RequestMapping(value="hello/{name:.*}") publicStringsayHello(@PathVariable("name")Stringname){ //该方法跳过通过上面描述的那种方式来确定MediaType request.setAttribute(PathExtensionContentNegotiationStrategy.class.getName()+".SKIP",true); //后面参数的值前半部分必须和该方法的RequestMapping一致,否则无效,不包括ContextPath request.setAttribute(WebUtils.INCLUDE_REQUEST_URI_ATTRIBUTE,"/hello/"+name+"/"); return"hello"+name; }
下面依赖源码来看一下为什么可以这么去做,先看一下为什么会造成这个结果?以下步骤只关心与当前问题有关的部分,并只大概关注其中问题,不作细节的深入
经过debug可以看到错误是在处理以下过程报错,首先如下
publicclassRequestResponseBodyMethodProcessorextendsAbstractMessageConverterMethodProcessor{ @Override publicvoidhandleReturnValue(ObjectreturnValue,MethodParameterreturnType, ModelAndViewContainermavContainer,NativeWebRequestwebRequest) throwsIOException,HttpMediaTypeNotAcceptableException,HttpMessageNotWritableException{ mavContainer.setRequestHandled(true); ServletServerHttpRequestinputMessage=createInputMessage(webRequest); ServletServerHttpResponseoutputMessage=createOutputMessage(webRequest); //Tryevenwithnullreturnvalue.ResponseBodyAdvicecouldgetinvolved. writeWithMessageConverters(returnValue,returnType,inputMessage,outputMessage); } }
出现这个问题,一般的查找思路就是是否是请求或响应的Content-Type是否出现了问题,那么在上面这个方法上无论是inputMessage还是outputMessage都是正常的,重点来看一下writeWithMessageConverters()方法,该方法,部分代码如下
publicabstractclassAbstractMessageConverterMethodProcessorextendsAbstractMessageConverterMethodArgumentResolver implementsHandlerMethodReturnValueHandler{ @SuppressWarnings("unchecked") protectedvoidwriteWithMessageConverters(Tvalue,MethodParameterreturnType, ServletServerHttpRequestinputMessage,ServletServerHttpResponseoutputMessage) throwsIOException,HttpMediaTypeNotAcceptableException,HttpMessageNotWritableException{ ObjectoutputValue; Class>valueType; TypedeclaredType; if(valueinstanceofCharSequence){ outputValue=value.toString(); valueType=String.class; declaredType=String.class; } else{ outputValue=value; valueType=getReturnValueType(outputValue,returnType); declaredType=getGenericType(returnType); } HttpServletRequestrequest=inputMessage.getServletRequest(); List requestedMediaTypes=getAcceptableMediaTypes(request); List producibleMediaTypes=getProducibleMediaTypes(request,valueType,declaredType); //后面处理MediaType的部分在这里全部省略 } /** *Returnsthemediatypesthatcanbeproduced: * *
*@since4.2 */ protectedList- Theproduciblemediatypesspecifiedintherequestmappings,or *
- Mediatypesofconfiguredconvertersthatcanwritethespecificreturnvalue,or *
- {@linkMediaType#ALL} *
getProducibleMediaTypes(HttpServletRequestrequest,Class>valueClass,TypedeclaredType){ Set mediaTypes=(Set )request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE); if(!CollectionUtils.isEmpty(mediaTypes)){ returnnewArrayList (mediaTypes); } elseif(!this.allSupportedMediaTypes.isEmpty()){ List result=newArrayList (); for(HttpMessageConverter>converter:this.messageConverters){ if(converterinstanceofGenericHttpMessageConverter&&declaredType!=null){ if(((GenericHttpMessageConverter>)converter).canWrite(declaredType,valueClass,null)){ result.addAll(converter.getSupportedMediaTypes()); } } elseif(converter.canWrite(valueClass,null)){ result.addAll(converter.getSupportedMediaTypes()); } } returnresult; } else{ returnCollections.singletonList(MediaType.ALL); } } }
先看方法getAcceptableMediaTypes(),是根据请求来决定当前的HttpServletRequest到底是要请求什么类型的数据,该方法调用链在后面说明;
getProducibleMediaTypes()方法返回可以生成的MediaType,能够生成哪些是看当前项目一共有多少可以被支持的MediaType,当然也能看到也可以通过HttpServletRequest明确设置属性HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE来确定用哪种方式;
拿到这两个列表后,需要判断requestedMediaTypes是否兼容producibleMediaTypes,如*/*则可以兼容所有的可以生成的MediaType,最终将兼容的requestedMediaTypes循环处理,看是否是一个具体的MediaType而不是通配符,那么最终生效的MediaType就是这个,当然存在多个,则也就存在多个不是通配也满足条件的,所以再循环前也做了一次排序,保证优先级最高的一定会生效。
publicabstractclassAbstractMessageConverterMethodProcessorextendsAbstractMessageConverterMethodArgumentResolver implementsHandlerMethodReturnValueHandler{ privateListgetAcceptableMediaTypes(HttpServletRequestrequest)throwsHttpMediaTypeNotAcceptableException{ List mediaTypes=this.contentNegotiationManager.resolveMediaTypes(newServletWebRequest(request)); return(mediaTypes.isEmpty()?Collections.singletonList(MediaType.ALL):mediaTypes); } }
MediaType.java
publicclassMediaTypeextendsMimeTypeimplementsSerializable{ publicstaticfinalMediaTypeALL; /** *AStringequivalentof{@linkMediaType#ALL}. */ publicstaticfinalStringALL_VALUE="*/*"; //静态初始化MediaType.ALL的值省略 }
该方法的结果可以看到如果调用的方法返回了一个空的列表,则该方法返回MediaType.ALL的列表,通过代码可以看到它的值为*/*,该方法往下调用部分代码如下:
publicclassContentNegotiationManagerimplementsContentNegotiationStrategy,MediaTypeFileExtensionResolver{ @Override publicListresolveMediaTypes(NativeWebRequestrequest)throwsHttpMediaTypeNotAcceptableException{ for(ContentNegotiationStrategystrategy:this.strategies){ List mediaTypes=strategy.resolveMediaTypes(request); if(mediaTypes.isEmpty()||mediaTypes.equals(MEDIA_TYPE_ALL)){ continue; } returnmediaTypes; } returnCollections.emptyList(); } }
调用如下:
publicclassWebMvcAutoConfiguration{ @Override publicListresolveMediaTypes(NativeWebRequestwebRequest) throwsHttpMediaTypeNotAcceptableException{ privatestaticfinalStringSKIP_ATTRIBUTE=PathExtensionContentNegotiationStrategy.class .getName()+".SKIP"; Objectskip=webRequest.getAttribute(SKIP_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST); if(skip!=null&&Boolean.parseBoolean(skip.toString())){ returnCollections.emptyList(); } returnthis.delegate.resolveMediaTypes(webRequest); } }
在这里可以看到有一个属性为skip,如果它的属性为PathExtensionContentNegotiationStrategy的类全名+".SKP"并且它的值为true,那么这里则不继续往下处理直接返回空的集合,而在前面也已经看到如果返回的空的集合,实际上最终返回给调用方的是*/*,结合前面看到的
org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor#writeWithMessageConverters(T,org.springframework.core.MethodParameter,org.springframework.http.server.ServletServerHttpRequest,org.springframework.http.server.ServletServerHttpResponse)
这个方法,*/*是可以匹配任何生成的producibleMediaTypes,所以最终结果能够按照原先应该返回的类型正确返回,而不会被.sh等后缀影响到;
其实最初没有看到skip的时候,看到了一些后面的代码,最终也解决了这个问题,不论正确与否,先把整个过程记录下来,假如在上面的步骤中没有设置skip=true,那么程序继续下去的部分走向如下
如果uid以.sh结尾的话,在逻辑处理完成之后框架处理return数据的时候,会根据扩展名来决定返回的content-type,sh结尾
会影响返回的content-type为application/x-sh,这会影响该方法的实际功能,解决办法是:
要么禁用该功能,要么修改该方法的@RequestMapping,禁用不能确定是否会对直接访问的静态资源有影响,
而且该方法调用方项目已上线,不宜轻易修改,只能这里改变这个属性的地址,影响框架
后面获取请求的后缀为null,而避免这个问题,但尚不能确认requestUrl和mappingUrl不一致是否会有别的问题
request.setAttribute(WebUtils.INCLUDE_REQUEST_URI_ATTRIBUTE,"/user/"+uid+"/");
以上为个人经验,希望能给大家一个参考,也希望大家多多支持毛票票。如有错误或未考虑完全的地方,望不吝赐教。
声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:czq8825#qq.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。