关于@PathVariable你需要知道的事

上问题

后端服务,通过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源码及注释

最佳实践

  1. 不反对使用@PathVariable,但是针对String类型的参数,我们需要保证不能带有特殊符号,尤其是/
  2. 如果参数内一定会有/等特殊字符,请使用@RequestParam,这种方式支持特殊字符。

参考

https://stackoverflow.com/questions/13482020/encoded-slash-2f-with-spring-requestmapping-path-param-gives-http-400

你可能感兴趣的:(关于@PathVariable你需要知道的事)