关于@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

返回报错

<!doctype html><html lang="en"><head><title>HTTP Status 400 – Bad Request</title><style type="text/css">body {font-family:Tahoma,Arial,sans-serif;} h1, h2, h3, b {color:white;background-color:#525D76;} h1 {font-size:22px;} h2 {font-size:16px;} h3 {font-size:14px;} p {font-size:12px;} a {color:black;} .line {height:1px;background-color:#525D76;border:none;}</style></head><body><h1>HTTP Status 400 – Bad Request</h1></body></html>%

问题解决一

对于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; j++, idx++ ) {
        if( buff[ j ] == '+' && query) {
            buff[idx]= (byte)' ' ;
        } else if( buff[ j ] != '%' ) {
            buff[idx]= buff[j];
        } else {
            // read next 2 digits
            // 查找%后2个字符
            if( j+2 >= 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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,755评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,369评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,799评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,910评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,096评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,159评论 3 411
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,917评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,360评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,673评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,814评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,509评论 4 334
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,156评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,882评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,123评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,641评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,728评论 2 351

推荐阅读更多精彩内容