http请求参数中巨坑的“+”被转为空格的问题

源码:https://gitee.com/qplo/rest-template
​ ,我们在通过 SpringBoot 提供的(客户端请求) RestTemplate 方法去请求其他服务的接口时,所带参数携带 “+” 号被转换成空格的问题

1.前提

  • 前提我们得先知道HTTP请求中携带的参数中带 “+” 请求后端时,在经过 tomcat 时,会被替换成空格。

通过debug我们来到

  • public final class Parameters 类中的 processParameters 方法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AOXocLnG-1640452473873)(C:\Users\deku\AppData\Roaming\Typora\typora-user-images\image-20211225232948655.png)]

经过层层套娃之后他就会进入下面的方法中

  • public final class UDecoder{} 类中的 convert 方法
    在这里插入图片描述

这里详细看https://www.cnblogs.com/thisiswhy/p/12119126.html

上面我们知道 “+” 在经过 tomcat 处理过后会变成空格

2.了解转码,解码

	String encode = URLEncoder.encode("come +here=", "UTF-8");
        System.out.println(encode);
        String decode = URLDecoder.decode(encode, "UTF-8");
        System.out.println(decode);
        String decodeDemo = URLDecoder.decode("come +here=/yuftujy", "UTF-8");
        System.out.println(decodeDemo);
/*
*	输出
*	come+%2Bhere%3D
*	come +here=
*	come  here=/yuftujy
*/
tomcat转码解码
“ ”“ ”“+”“ ”
“+”“ ”“%2B”“+”
“=”“=”“%3D”“=”

我们这就发现一个空格的问题,他在转码后没有变成我们所预期的 “%20”,而是变成了 ”+“,而 ”+” 号在经过 tomcat 时又会变成 空格?

这会导致什么问题,后端怎么去处理空格?

这就是一个巨大的坑!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

3.发现问题

现在我们结合工作中的问题,在客户端,用SpringBoot提供的 RestTemplate 请求其他服务参数带 ”+“ 的问题、

  • 得知 admId 从请求开始到结束一共经过了两次 tomcat

在调试中我发现三种情况

1、正常请求:http://localhost:8080/cdata?admId=S9Y+qEERxxGVFo0DE8mruGVGmVYNJraVwZyijimEv6w=

@GetMapping("/cdata")
public Object cdata(@RequestParam String admId) throws Exception{
    
    System.out.println("tomcat转:"+admId);//S9Y qEERxxGVFo0DE8mruGVGmVYNJraVwZyijimEv6w=

    String url="http://localhost:8080/hello";
    UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromHttpUrl(url)
            .queryParam("admId", encode);
    String urls = uriComponentsBuilder.build().toUriString();

    HttpEntity<Map<String, Object>> requestEntity = new HttpEntity<>(null, null);
    ResponseEntity<ResponsVO> response = restTemplate.exchange(urls, HttpMethod.GET, requestEntity,
            new ParameterizedTypeReference<ResponsVO>() {
            });

    return response.getBody().getData();
}

@GetMapping("hello")
public Object cdataDemo(String admId) throws Exception{
    
    System.out.println(admId);//S9Y qEERxxGVFo0DE8mruGVGmVYNJraVwZyijimEv6w=
    
    ResponsVO<Object> responsVO = new ResponsVO<>();
    responsVO.setCode(200);
    responsVO.setMsg("成功");
    User user = new User();
    user.setId(1);
    user.setName("");
    responsVO.setData(user);
    return responsVO;
}

//正常请求我们可以看到参数 admId 第一次进过tomcat的时候 “+” 就变成了空格,肯定是不对的

2.第二种:加上转码解码

 @GetMapping("/cdata")
    public Object cdata(@RequestParam String admId) throws Exception{
        
        System.out.println("转码前:"+admId);//转码前:S9Y qEERxxGVFo0DE8mruGVGmVYNJraVwZyijimEv6w=

        String encode = URLEncoder.encode(admId, "UTF-8");
        
        System.out.println("转码后:"+encode);//转码后:S9Y+qEERxxGVFo0DE8mruGVGmVYNJraVwZyijimEv6w%3D

        String url="http://localhost:8080/hello";
        UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromHttpUrl(url)
                .queryParam("admId", encode);
        String urls = uriComponentsBuilder.build().toUriString();
        HttpEntity<Map<String, Object>> requestEntity = new HttpEntity<>(null, null);
        ResponseEntity<ResponsVO> response = restTemplate.exchange(urls, HttpMethod.GET, requestEntity,
                new ParameterizedTypeReference<ResponsVO>() {
                });

        return response.getBody().getData();
    }

    @GetMapping("hello")
    public Object cdataDemo(String admId) throws Exception{
        
        System.out.println("解码前:"+admId);
        //解码前:S9Y qEERxxGVFo0DE8mruGVGmVYNJraVwZyijimEv6w%3D
        System.out.println("解码后:"+URLDecoder.decode(admId,"UTF-8"));
        //解码后:S9Y qEERxxGVFo0DE8mruGVGmVYNJraVwZyijimEv6w=
        
        ResponsVO<Object> responsVO = new ResponsVO<>();
        responsVO.setCode(200);
        responsVO.setMsg("成功");
        User user = new User();
        user.setId(1);
        user.setName("");
        responsVO.setData(user);
        return responsVO;
    }
} 
/**
 * 转码前:S9Y qEERxxGVFo0DE8mruGVGmVYNJraVwZyijimEv6w=
 * 转码后:S9Y+qEERxxGVFo0DE8mruGVGmVYNJraVwZyijimEv6w%3D
 * 解码前:S9Y qEERxxGVFo0DE8mruGVGmVYNJraVwZyijimEv6w%3D
 * 解码后:S9Y qEERxxGVFo0DE8mruGVGmVYNJraVwZyijimEv6w=
 */
  • 还是不行,因为 admId经过第一次 tomcat的时候就已经把“+”替换成了空格,转码后“=”被符合预期的转成了"%3D",但是 空格 被转码后又变成了 “+” ,不是我们所预期的“%20”,这导致在第二次请求接口的时候 “+” 又被替换成了空格。
  • 这么一看,那他不是无解了吗???
  • 还有一种不得已的办法,直接把空格替换成 “+”

或者

3.第三种:前端传参时进行转码-S9Y%2BqEERxxGVFo0DE8mruGVGmVYNJraVwZyijimEv6w%3D

@GetMapping("/cdata")
    public Object cdata(@RequestParam String admId) throws Exception{
//        admId="admId%2BS9Y%2BqEERxxGVFo0DE8mruGVGmVYNJraVwZyijimEv6w%3D";
        
        System.out.println("转码前:"+admId);//转码前:S9Y+qEERxxGVFo0DE8mruGVGmVYNJraVwZyijimEv6w=
        String encode = URLEncoder.encode(admId, "UTF-8");
        System.out.println("转码后:"+encode);//转码后:S9Y%2BqEERxxGVFo0DE8mruGVGmVYNJraVwZyijimEv6w%3D

        String url="http://localhost:8080/hello";
        UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromHttpUrl(url)
                .queryParam("admId", encode);
        String urls = uriComponentsBuilder.build().toUriString();
        HttpEntity<Map<String, Object>> requestEntity = new HttpEntity<>(null, null);
        ResponseEntity<ResponsVO> response = restTemplate.exchange(urls, HttpMethod.GET, requestEntity,
                new ParameterizedTypeReference<ResponsVO>() {
                });

        return response.getBody().getData();
    }

    @GetMapping("hello")
    public Object cdataDemo(String admId) throws Exception{
        System.out.println("解码前:"+admId);
        //解码前:S9Y%2BqEERxxGVFo0DE8mruGVGmVYNJraVwZyijimEv6w%3D
        System.out.println("解码后:"+URLDecoder.decode(admId,"UTF-8"));
        //解码后:S9Y+qEERxxGVFo0DE8mruGVGmVYNJraVwZyijimEv6w=
        ResponsVO<Object> responsVO = new ResponsVO<>();
        responsVO.setCode(200);
        responsVO.setMsg("成功");
        User user = new User();
        user.setId(1);
        user.setName("");
        responsVO.setData(user);
        return responsVO;
    }
/**
 * 转码前:S9Y+qEERxxGVFo0DE8mruGVGmVYNJraVwZyijimEv6w=
 * 转码后:S9Y%2BqEERxxGVFo0DE8mruGVGmVYNJraVwZyijimEv6w%3D
 * 解码前:S9Y%2BqEERxxGVFo0DE8mruGVGmVYNJraVwZyijimEv6w%3D
 * 解码后:S9Y+qEERxxGVFo0DE8mruGVGmVYNJraVwZyijimEv6w=
 */
  • 从输出结果中我们可以看出,前端对参数进行转码后,“+” 被符合预期的转成了 “%2B”,进过tomcat的解码之后,再对其进行转码,在其他服务接口对其进行解码之后就能返回正常数据

  • 这里又发现一个问题,这次我们可以看到,第一次请求时,前端转码后的参数传到后端时被自动解码了,但是在第二次发起请求时,参数没有被自动解码,需要手动解码???

  • 原因:是因为 restTemplate 的问题,因为 restTemplate 发起get请求时,会对参数进行一次编码,tomcat又对其进行解码。但是这时候就会发现既然 restTemplate 会对其进行转码,那为什么还要用 encode 再对其进行一次转码? 因为restTemplate 的编码和 encode有所不同,restTemplate 不会对某些字符进行编码,例如 “+” 等,导致“+”在经过 tomcat 时被转成空格!!!
    https://www.jianshu.com/p/0bdcc6836eb3
    在这里插入图片描述
    总结:

  • 前端传参到后端,参数首先会经过 tomcat 的检验,tomcat 的 convert 的方法会对参数进行解码,并把“+”提换成空格

  • 如果用 restTemplate 去请求其他服务时,参数又会经过 restTemplate 的检验,restTemplate 会对其进行编码,但是有一点不同的是,不会对某些特殊字符进行编码。例如 “+” 等。

  • 请求到其他服务时参数又来到了 tomcat 这里经受检验,然后来到 controller 层

用 restTemplate 请求其他服务 的参数带有特殊字符时

解决办法:
1.

  • 前端对参数进行转码(把“+”转化为“%2B”)
  • 后端对参数进行一次转码,
  • 其他服务进行解码

前端转两次码
其他服务进行解码

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐