上问题
后端服务,通过productCode获取Product
@GetMapping("/product/{productCode}")
public String getProduct(@PathVariable("productCode") String productCode){
System.out.println(productCode);
return "hello";
}
模拟前端请求
curl http://localhost:8080/product/123%2Fxxx
模拟前端调用,因为我的参数里带了/
,所以请求的时候会自动转义成%2F
返回报错
HTTP Status 400 – Bad Request HTTP Status 400 – Bad Request
%
问题解决一
对于productCode来讲,一般是按照固定的规则生成,不可能带有/
,.
,-
等特殊字符。
但是我们的业务上,确实遇到了这奇葩的场景。
额,本文只探讨技术问题,不探讨产品实现。
经过一轮的DEBUG,发现tomcat中对于url会进行校验。
方法坐标为 org.apache.tomcat.util.buf.UDecoder#convert(org.apache.tomcat.util.buf.ByteChunk, boolean, org.apache.tomcat.util.buf.EncodedSolidusHandling)
private void convert(ByteChunk mb, boolean query, EncodedSolidusHandling encodedSolidusHandling) throws IOException {
int start=mb.getOffset();
byte buff[]=mb.getBytes();
int end=mb.getEnd();
//查找%的位置
int idx= ByteChunk.findByte( buff, start, end, (byte) '%' );
int idx2=-1;
if( query ) {
idx2= ByteChunk.findByte( buff, start, (idx >= 0 ? idx : end), (byte) '+' );
}
if( idx<0 && idx2<0 ) {
return;
}
// idx will be the smallest positive index ( first % or + )
if( (idx2 >= 0 && idx2 < idx) || idx < 0 ) {
idx=idx2;
}
for( int j=idx; j= end ) {
throw EXCEPTION_EOF;
}
byte b1= buff[j+1];
byte b2=buff[j+2];
//判断%后面必须为16进制的数字或字符
if( !isHexDigit( b1 ) || ! isHexDigit(b2 )) {
throw EXCEPTION_NOT_HEX_DIGIT;
}
j+=2;
//获取b1,b2拼接而成的ascii码
int res=x2c( b1, b2 );
// 如果res为/对应的ascii码
if (res == '/') {
//处理策略
switch (encodedSolidusHandling) {
//转换成/
case DECODE: {
buff[idx]=(byte)res;
break;
}
//拒绝,抛异常
case REJECT: {
throw EXCEPTION_SLASH;
}
//跳过,啥也不做
case PASS_THROUGH: {
idx += 2;
}
}
} else {
buff[idx]=(byte)res;
}
}
}
mb.setEnd( idx );
}
显而易见,tomcat的默认策略是拒绝,所以导致了我们调用的异常。
所以我们要想办法把这个策略修改为DECODE
或者PASS_THROUGH
。
经过追踪。
发现convert
方法的encodedSolidusHandling
入参来自于org.apache.catalina.connector.Connector#encodedSolidusHandling
private EncodedSolidusHandling encodedSolidusHandling =
UDecoder.ALLOW_ENCODED_SLASH ? EncodedSolidusHandling.DECODE : EncodedSolidusHandling.REJECT;
而UDecoder.ALLOW_ENCODED_SLASH
来自于
@Deprecated
public static final boolean ALLOW_ENCODED_SLASH =
Boolean.parseBoolean(System.getProperty("org.apache.tomcat.util.buf.UDecoder.ALLOW_ENCODED_SLASH", "false"));
可以看到ALLOW_ENCODED_SLASH
取自于系统配置org.apache.tomcat.util.buf.UDecoder.ALLOW_ENCODED_SLASH
,默认为false
,也就是encodedSolidusHandling
默认为EncodedSolidusHandling.REJECT
。
因此,解决方式就是,在我们SpringBoot项目启动类的main函数中加上以下代码
System.setProperty("org.apache.tomcat.util.buf.UDecoder.ALLOW_ENCODED_SLASH", "true");
或者增加环境变量
-Dorg.apache.tomcat.util.buf.UDecoder.ALLOW_ENCODED_SLASH=true
问题解决二
你以为问题就这样解决了?
还是返回了以下的错误
{"timestamp":1606463869830,"status":404,"error":"Not Found","message":"","path":"/product/123%2Fxx"}%
虽然tomcat绕了过去,但是在springmvc这边,我们拿到的path,会进行decode,也就是/product/123/xx
,也是匹配不到我们接口上配置的路径/product/{productCode}
。
关于spring匹配逻辑,见org.springframework.util.AntPathMatcher源码及注释
最佳实践
- 不反对使用@PathVariable,但是针对String类型的参数,我们需要保证不能带有特殊符号,尤其是
/
。 - 如果参数内一定会有
/
等特殊字符,请使用@RequestParam,这种方式支持特殊字符。
参考
https://stackoverflow.com/questions/13482020/encoded-slash-2f-with-spring-requestmapping-path-param-gives-http-400