如何处理@PathVariable中的特殊字符问题

上代码:

 @GetMapping(value="/user/{useraccount}")
 public void getUserAccount(@PathVariable("useraccount") String userAccount) {

 logger.info("useraccount :" + userAccount);
 }

正常访问:

/user/zhangsan

打印:useraccount : zhangsan

看似一切正常

but:

访问:/user/zhangsan/lisi

打印:useraccount : zhangsan

咦,为啥不是useraccount :zhangsan/lisi ?

@PathVariable并没有我们想象的聪明,对于参数中的/并不能跟实际路径/分开

事实上,有. ; -等都不能正确切分。

怎么办呢?

两种方案:

1,简单点,直接使用@RequestParam代替

 @GetMapping(value="/user")
 public void getUserAccount(@RequestParam("useraccount") String userAccount) {

 logger.info("useraccount :" + userAccount);
 }

用/user?useraccount=zhangsan 访问

2,使用正则过滤

 @GetMapping(value="/user/{useraccount:[a-zA-Z0-9\\.\\-\\_\\;\\\]+}")
 public void getUserAccount(@PathVariable("useraccount") String userAccount) {

 logger.info("useraccount :" + userAccount);
 }

正常访问:

/user/zhangsan

打印:useraccount : zhangsan

当然,这个就有点不灵活了,第一种简单又方便

补充:记一次@PathVariable特殊参数会丢失的排查问题

请求参数中如果包含.,会造成参数丢失,请看如下代码

以下代码,省略@RestController控制层类代码

@RequestMapping(value = "hello/{name}")
public Map<String, Object> sayHello(@PathVariable("name") String name, HttpServletRequest request) {
 Map<String, Object> rtnMap = new HashMap<>();
 rtnMap.put("msg", "hello " + name);
 return rtnMap;
}

请求地址: hello/ddf,则正常返回{"msg": "hello ddf"}

请求地址: hello/ddf.com,依然还是返回{"msg": "hello ddf"}

如果需要解决上面这个问题,则可以将代码更改如下(该解决方式从网上搜寻)

@RequestMapping(value = "hello/{name:.*}")
public Map<String, Object> sayHello(@PathVariable("name") String name, HttpServletRequest request) {
 Map<String, Object> rtnMap = new HashMap<>();
 rtnMap.put("msg", "hello " + name);
 return rtnMap;
}

如果使用@PathVariable以.sh或.bat等特殊字符结尾,会影响实际返回数据

报错如下:

{
 "timestamp": 1541405292119,
 "status": 406,
 "error": "Not Acceptable",
 "exception": "org.springframework.web.HttpMediaTypeNotAcceptableException",
 "message": "Could not find acceptable representation",
 "path": "/HDOrg/user/hello/ddf.sh"
}

还是上面的代码

以下代码,省略@RestController控制层类代码

@RequestMapping(value = "hello/{name:.*}")
public Map<String, Object> sayHello(@PathVariable("name") String name, HttpServletRequest request) {
 Map<String, Object> rtnMap = new HashMap<>();
 rtnMap.put("msg", "hello " + name);
 return rtnMap;
}

如果这时候请求地址为hello/ddf.sh或hello/ddf.com.sh,只要是以.sh结尾,这时候业务逻辑代码不会受到影响,但走到Spring自己的代码去处理返回数据的时候,有一个功能会根据扩展名来决定返回的类型,而以.sh结尾扩展名为sh,会被解析成对应的Content-Type: application/x-sh。

解决办法如下,第一种方法是从网上找到的,可以直接禁用该功能,但有可能会影响到静态资源的访问,不能确定,也没有进行尝试

@Configuration
public class Config extends WebMvcConfigurerAdapter {
 @Override
 public void configureContentNegotiation(
  ContentNegotiationConfigurer configurer) {
 configurer.favorPathExtension(false);
 }
}

然后以下就是闲着没事很想换个思路尝试去看看这到底是怎么回事,由于个人能力有限,不保证以下内容的重要性;

第二种方式解决思路是,既然扩展名以.sh等结尾会有问题,那么能不能不要让程序将扩展名识别为.sh,或者干脆就跳过处理,比如我是否可以加个.sh/这样就会影响到实际的扩展名,但是又不会影响到已有的代码,其实这里有个偷懒的写法,可以直接在@RequestMapping里的value最后直接加一个/,但是这要求客户端必须在原有的条件上最终拼一个/,否则会找不到对应的映射,直接404,我这里碰到这个问题的时候,因为该方法已经上线并且被其它几个系统调用,因此更改起来会有些繁琐,所以寻求了一种麻烦的方式,先将解决方式放在下面,不确定是否会影响其它问题

这种方式解决方式如下:注释中的两行代码二选一都可,推荐前面的写法,直接已经跳过

@RequestMapping(value = "hello/{name:.*}")
public String sayHello(@PathVariable("name") String name) {
 // 该方法跳过通过上面描述的那种方式来确定MediaType
 request.setAttribute(PathExtensionContentNegotiationStrategy.class.getName() + ".SKIP", true);
 // 后面参数的值前半部分必须和该方法的RequestMapping一致,否则无效,不包括ContextPath
 request.setAttribute(WebUtils.INCLUDE_REQUEST_URI_ATTRIBUTE, "/hello/" + name + "/");
 return "hello " + name;
}

下面依赖源码来看一下为什么可以这么去做,先看一下为什么会造成这个结果?以下步骤只关心与当前问题有关的部分,并只大概关注其中问题,不作细节的深入

经过debug可以看到错误是在处理以下过程报错,首先如下

public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {
 @Override
 public void handleReturnValue(Object returnValue, MethodParameter returnType,
  ModelAndViewContainer mavContainer, NativeWebRequest webRequest)
  throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
 mavContainer.setRequestHandled(true);
 ServletServerHttpRequest inputMessage = createInputMessage(webRequest);
 ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);
 // Try even with null return value. ResponseBodyAdvice could get involved.
 writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
 }
}

出现这个问题,一般的查找思路就是是否是请求或响应的Content-Type是否出现了问题,那么在上面这个方法上无论是inputMessage还是outputMessage都是正常的,重点来看一下writeWithMessageConverters()方法,该方法,部分代码如下

public abstract class AbstractMessageConverterMethodProcessor extends AbstractMessageConverterMethodArgumentResolver
 implements HandlerMethodReturnValueHandler {
 @SuppressWarnings("unchecked")
 protected <T> void writeWithMessageConverters(T value, MethodParameter returnType,
       ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage)
 throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
 Object outputValue;
 Class<?> valueType;
 Type declaredType;
 if (value instanceof CharSequence) {
  outputValue = value.toString();
  valueType = String.class;
  declaredType = String.class;
 }
 else {
  outputValue = value;
  valueType = getReturnValueType(outputValue, returnType);
  declaredType = getGenericType(returnType);
 }
 HttpServletRequest request = inputMessage.getServletRequest();
 List<MediaType> requestedMediaTypes = getAcceptableMediaTypes(request);
 List<MediaType> producibleMediaTypes = getProducibleMediaTypes(request, valueType, declaredType);
 // 后面处理MediaType的部分在这里全部省略
 } 

 /**
 * Returns the media types that can be produced:
 * <ul>
 * <li>The producible media types specified in the request mappings, or
 * <li>Media types of configured converters that can write the specific return value, or
 * <li>{@link MediaType#ALL}
 * </ul>
 * @since 4.2
 */
 protected List<MediaType> getProducibleMediaTypes(HttpServletRequest request, Class<?> valueClass, Type declaredType) {
 Set<MediaType> mediaTypes = (Set<MediaType>) request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
 if (!CollectionUtils.isEmpty(mediaTypes)) {
  return new ArrayList<MediaType>(mediaTypes);
 }
 else if (!this.allSupportedMediaTypes.isEmpty()) {
  List<MediaType> result = new ArrayList<MediaType>();
  for (HttpMessageConverter<?> converter : this.messageConverters) {
  if (converter instanceof GenericHttpMessageConverter && declaredType != null) {
   if (((GenericHttpMessageConverter<?>) converter).canWrite(declaredType, valueClass, null)) {
   result.addAll(converter.getSupportedMediaTypes());
   }
  }
  else if (converter.canWrite(valueClass, null)) {
   result.addAll(converter.getSupportedMediaTypes());
  }
  }
  return result;
 }
 else {
  return Collections.singletonList(MediaType.ALL);
 }
 }
}

先看方法getAcceptableMediaTypes(),是根据请求来决定当前的HttpServletRequest到底是要请求什么类型的数据,该方法调用链在后面说明;

getProducibleMediaTypes()方法返回可以生成的MediaType,能够生成哪些是看当前项目一共有多少可以被支持的MediaType,当然也能看到也可以通过HttpServletRequest明确设置属性HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE来确定用哪种方式;

拿到这两个列表后,需要判断requestedMediaTypes是否兼容producibleMediaTypes,如*/*则可以兼容所有的可以生成的MediaType,最终将兼容的requestedMediaTypes循环处理,看是否是一个具体的MediaType而不是通配符,那么最终生效的MediaType就是这个,当然存在多个,则也就存在多个不是通配也满足条件的,所以再循环前也做了一次排序,保证优先级最高的一定会生效。

public abstract class AbstractMessageConverterMethodProcessor extends AbstractMessageConverterMethodArgumentResolver
 implements HandlerMethodReturnValueHandler {
 private List<MediaType> getAcceptableMediaTypes(HttpServletRequest request) throws HttpMediaTypeNotAcceptableException {
 List<MediaType> mediaTypes = this.contentNegotiationManager.resolveMediaTypes(new ServletWebRequest(request));
 return (mediaTypes.isEmpty() ? Collections.singletonList(MediaType.ALL) : mediaTypes);
 }
}

MediaType.java

public class MediaType extends MimeType implements Serializable {
 public static final MediaType ALL;
 /**
 * A String equivalent of {@link MediaType#ALL}.
 */
 public static final String ALL_VALUE = "*/*";

 // 静态初始化MediaType.ALL的值省略
}

该方法的结果可以看到如果调用的方法返回了一个空的列表,则该方法返回MediaType.ALL的列表,通过代码可以看到它的值为*/*,该方法往下调用部分代码如下:

public class ContentNegotiationManager implements ContentNegotiationStrategy, MediaTypeFileExtensionResolver {

 @Override
 public List<MediaType> resolveMediaTypes(NativeWebRequest request) throws HttpMediaTypeNotAcceptableException {
 for (ContentNegotiationStrategy strategy : this.strategies) {
  List<MediaType> mediaTypes = strategy.resolveMediaTypes(request);
  if (mediaTypes.isEmpty() || mediaTypes.equals(MEDIA_TYPE_ALL)) {
  continue;
  }
  return mediaTypes;
 }
 return Collections.emptyList();
 }
}

调用如下:

public class WebMvcAutoConfiguration {
 @Override
 public List<MediaType> resolveMediaTypes(NativeWebRequest webRequest)
 throws HttpMediaTypeNotAcceptableException {

 private static final String SKIP_ATTRIBUTE = PathExtensionContentNegotiationStrategy.class
  .getName() + ".SKIP";

 Object skip = webRequest.getAttribute(SKIP_ATTRIBUTE,
      RequestAttributes.SCOPE_REQUEST);
 if (skip != null && Boolean.parseBoolean(skip.toString())) {
  return Collections.emptyList();
 }
 return this.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 + "/");

以上为个人经验,希望能给大家一个参考,也希望大家多多支持我们。如有错误或未考虑完全的地方,望不吝赐教。

(0)

相关推荐

  • 聊聊@RequestParam,@PathParam,@PathVariable等注解的区别

    @RequestParam 和 @PathVariable 注解是用于从request中接收请求的,两个都可以接收参数,关键点不同的是@RequestParam 是从request里面拿取值,而 @PathVariable 是从一个URI模板里面来填充 @RequestParam 看下面一段代码: http://localhost:8080/springmvc/hello/101?param1=10&param2=20 根据上面的这个URL,你可以用这样的方式来进行获取 public Strin

  • 解决@PathVariable对于特殊字符截断的问题

    概述: @ResponseBody @RequestMapping(value="/download/{fileName:[a-zA-Z0-9\\.\\-\\_]+}", method = RequestMethod.GET) public void downloadAmr( HttpServletRequest request, HttpServletResponse response, @PathVariable("fileName") String fileN

  • @PathVariable注解,让spring支持参数带值功能的案例

    @PathVariable的作用 获取URL动态变量,例如 @RequestMapping("/users/{userid}") @ResponseBody public String getUser(@PathVariable String userid){ return "userid=" + userid; } @PathVariable的包引用 spring自从3.0版本就引入了org.springframework.web.bind.annotation.

  • spring boot @PathVariable传递带反斜杠参数 / 的处理

    我就废话不多说了,大家还是看完整的代码吧~ @RequestMapping(value = "/modules/{moduleBaseName}/**", method = RequestMethod.GET) @ResponseBody public String moduleStrings(@PathVariable String moduleBaseName, HttpServletRequest request) { final String path = request.ge

  • 解决springmvc使用@PathVariable路径匹配问题

    一.问题 今天作毕设的时候,在搭建ssm框架的使用使用springmvc的@PathVariable时出现了一个路径匹配的问题,最后花了点时间解决了. 代码结构: 问题内容: 访问url为: 按照道理说,我应该到jsp的index页面去.最后的结果确实到了index页面,可是由于该页面引用了几个css和js,报异常找不到.有使用过spring经验的童鞋应该知道使用如下代码解决静态资源的访问. // 方法一 <mvc:default-servlet-handler/> // 方法二 <mv

  • 处理@PathVariable注解允许参数为空、允许不传参数的问题

    说明 设置可以不传: 但是请求的时候,仍然提示projectId必传: 这是因为url要求带参,属于REST参数. 解决办法 设置多个url path即可,有的传REST参,有的不传.当然required必须是false 这个问题以前解决过,--还是要温习一下比较好呀 补充:接收参数注解@PathVariable 与 @RequestParam与 不使用注解接收参数的情况 @RequestParam使用场景: @RequestParam: 顾名思义,获取请求参数的 @RequestParam的两

  • 如何处理@PathVariable中的特殊字符问题

    上代码: @GetMapping(value="/user/{useraccount}") public void getUserAccount(@PathVariable("useraccount") String userAccount) { logger.info("useraccount :" + userAccount); } 正常访问: /user/zhangsan 打印:useraccount : zhangsan 看似一切正常 b

  • 如何处理JSON中的特殊字符

    JSON 是适用于 Ajax 应用程序的一种有效格式,原因是它使 JavaScript 对象和字符串值之间得以快速转换.由于 Ajax 应用程序非常适合将纯文本发送给服务器端程序并对应地接收纯文本,相比不能生成文本的 API,能生成文本的 API 自然更可取:而且,JSON 让您能够处理本地 JavaScript 对象,而无需为如何表示这些对象多费心思. XML 也可以提供文本方面的类似益处,但用于将 JavaScript 对象转换成 XML 的几个现有 API 没有 JSON API 成熟:有

  • Shell脚本中的特殊字符(美元符、反斜杠、引号等)作用介绍

    Shell中的特殊字符有 1.$ 美元符 2.\ 反斜杠 3.` 反引号 4." 双引号 5.< ,>;,*,?,[,] 下面我一一举列说明 一.$符号 1.echo $? 显示的是上一条指令退出状态 2.echo "$?" 效果同上 3.echo '$?' 显示的是$? 4.echo \$? 显示的是$? 5.echo "\$?" 显示的是$? 大家可能已经看出 $符号在双引号中具有特殊意义 双引号对$符号不起作用 而单引号可以将特殊字符的的

  • 使用正则表达式替换报表名称中的特殊字符(推荐)

    正则表达式,又称规则表达式.(英语:Regular Expression,在代码中常简写为regex.regexp或RE),计算机科学的一个概念.正则表通常被用来检索.替换那些符合某个模式(规则)的文本. 许多程序设计语言都支持利用正则表达式进行字符串操作.例如,在Perl中就内建了一个功能强大的正则表达式引擎,还有java语言自带的.正则表达式这个概念最初是由Unix中的工具软件(例如sed和grep)普及开的.正则表达式通常缩写成"regex",单数有regexp.regex,复数

  • 不要在cookie中使用特殊字符的原因分析

    Constructs a cookie with a specified name and value. The name must conform to RFC 2109. That means it can contain only ASCII alphanumeric characters and cannot contain commas, semicolons, or white space or begin with a $ character. The cookie's name

  • js中传递特殊字符(+,&)的方法

    背景: 今天在做一个任务时,用Jquery的Ajax传递一长串字符时,在后台的验证一直不成功,纠结时我了(那个字符串是随机生成的,特长).查了一上午,原来是我生成的字符串中有+号,而在js传递的时候,会理解为是连接字符用的,到了后台就将+号自动变为空格了,所以后台的字符串和前台生成的已经不一样了. 原因: js后自动解析特殊字符,如+号为连接符,解析为空格,&为变量连接符,服务器端接受数据时&以后的数据不显示等等. 解决办法: 1.将字符放到form中,然后用js提交form表单到服务器.

  • 数据库查询中遭遇特殊字符导致问题的解决方法

    数据库查询中的特殊字符的问题 在进行数据库的查询时,会经常遇到这样的情况:  例如想在一个用户数据库中查询他的用户名和他的密码,但恰好该用户使用的名字和密码中有特殊的 字符,例如单引号,"|"号,双引号或者连字符"&".  例如他的名字是1"test,密码是A|&900  这时当你执行以下的查询语句时,肯定会报错: SQL = "SELECT * FROM SecurityLevel WHERE UID="" 

  • CI(CodeIgniter)框架中URL特殊字符处理与SQL注入隐患分析

    本文实例分析了CI(CodeIgniter)框架中URL特殊字符处理与SQL注入隐患.分享给大家供大家参考,具体如下: php CI框架中URL特殊字符有很多是不支持的,导致像c++,括号这些常用的分类,字符都无法正常显示很头痛,而在配置里增加单引号' 反斜杠\ 这种特殊字符又很容易给sql注入 在默认的config配置基础上加上:+=()特殊字符 #$config['permitted_uri_chars'] = 'a-z 0-9~%.:_\-'; $config['permitted_uri

  • python3如何去除字符串中的特殊字符

    在Python中用replace()函数操作指定字符 常用字符unicode的编码范围: 数字:\u0030-\u0039 汉字:\u4e00-\u9fa5 大写字母:\u0041-\u005a 小写字母:\u0061-\u007a 英文字母:\u0041-\u007a 1.将字符串中的指定符号替换成指定符号 #将old字符串中的2替换为9 old = "bcsbviuwvb123221iuw" new = old .replace('2', '9') 2.字符串删掉空格 str.st

  • mysql如何处理varchar与nvarchar类型中的特殊字符

    如果你每次建数据表的时候固执的使用varchar,那么你可能会遇到以下的问题: 现在saleUserName的字段类型为varchar(50) update TableNameset saleUserName='小覃祝你⑭快乐' where ID=87 select * from TableName where ID=87 why?SaleUserName字段里的文字怎么变成这样了.⑭这个符号怎么变成了? 好的,那么我现在将saleUserName的字段类型改为nvarchar(50)呢 upd

随机推荐