关于SpringBoot项目使用undertow容器的中文参数乱码问题的真正解决方案

实验项目 SpringBoot 版本为 2.3.5.RELEASE

如若担心其他版本是否适用本方案,请查看文章 兼容性 章节

一、概述

来到这里的朋友,你一定遇见了中文参数乱码的问题。

你是否有以下症状:

  • 项目已设置了 server.servlet.encoding.charset=utf-8server.servlet.encoding.force=true 还是会有乱码
  • 项目在上条配置的基础上加上了 server.undertow.urlCharset=utf-8 还是会有乱码
  • 项目在上述配置下有的接口正常但是有的接口乱码

二、事故现场搭建

我们先来搭建一下现场以便还原事故

pom.xml 配置如下即可:



    
    
        org.springframework.boot
        spring-boot-starter-parent
        2.3.5.RELEASE
    
    
    
        
            org.springframework.boot
            spring-boot-starter-web
            
                
                    org.springframework.boot
                    spring-boot-starter-tomcat
                
            
        
        
            org.springframework.boot
            spring-boot-starter-undertow
        
    

springboot启动类:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class App {
    public static void main(String[] args) {
        SpringApplication.run(App.class);
    }
}

请求controller,注意启动类没有添加@ComponentScan 注解,要把controller放到启动类同目录或者下一级目录下,这个功能是返回给定的姓名

import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;

@RestController
public class DemoController {

    @RequestMapping("/a")
    @CrossOrigin
    public String echoName(HttpServletRequest request) {
        String name = request.getParameter("name");
        System.out.println(name); //打印一下,debug的话可以不用打印也能看到
        return name;
    }
}

至此搭建完毕,启动项目。

三、事故还原

1、普通get请求

地址栏普通get请求
咦,居然是正常的

2、普通post请求

post请求html页面代码



    
        
    
    
        

请求结果:
post请求正常返回
咦,居然也是正常的

3、ajax请求

ajax请求源码



    
        
    
    
        
    

请求结果:
ajax请求乱码
这里是乱码了

四、事故分析之参数传递

可以看到,借用浏览器的普通get\post请求,都能返回正常无乱码结果。

也许你的浏览器返回的是乱码结果,不过这是合理的。

这里说一下为什么能返回正常结果:得益于现在浏览器的智能行为,它对你的参数进行了隐式的URL编码转换。

F12打开浏览器控制台,看get的原报文:
浏览器控制台get请求原始报文
蓝色选中Method上方,是请求的url,可以看到参数 name=张三已经被隐式地转码了。同理,post也是,上述post请求浏览器控制台最后一行已经很清晰地显示了转码后的参数。

这一点,对于新手或者不熟悉前端知识的后端开发人员来说,很容易让人解决乱码的时候无从下手。

这也就明白了,为什么ajax请求返回的,是乱码,因为它的参数没有得到浏览器的URL编码转换。这是我们期望的,也就是上述说的乱码结果是合理的。

也许有些人会说,那我只要在ajax请求里对参数转码就好了,这里给出一个建议:

尽量对所有参数进行URL转码,除非你很清楚它只有字母和数字

浏览器也是人创造的,万一哪一天,它不再偷偷给你转码了呢?

五、事故分析之参数接收

我们debug去查看应用后台的参数接受情况,参数接受代码这样:

String name = request.getParameter("name");

debug得知,当request中不存在key为name 的参数时候,会从容器中获取字节,然后进行form表单数据的转换。

其中undertow对http请求字节的转换处理,在io.undertow.server.handlers.form.FormEncodedDataDefinition.doParse() 方法中:

private void doParse(final StreamSourceChannel channel) throws IOException {
    //省略部分代码
    final ByteBuffer buffer = pooled.getBuffer();
    //省略部分代码
    byte n = buffer.get();
    //省略部分代码
    builder.append((char) n); //关键操作,对字节直接强转为char,这也就是接受到参数为乱码的原因
    //省略部分代码
    addPair(name, builder.toString()); //把转换后的参数存储起来,最终会存放到request中
    //省略部分代码
}

由上述代码得知,undertow对我们的参数直接进行了强制char型转换,而不是由字节转到字符串,导致request中获取到的参数为强转后乱码的原因。而且undertow官方认为这不是个错误,拒绝修复。泱泱大国,遭受歧视,努力奋斗吧骚年,让我大中华民族在科技界不再遭受忽略、排挤、打击的日子早点到来。

就没有别的办法了吗?

六、解决办法

办法还是有的。可以看到,byte直接转成了char,没有中间操作,不存在高低补位的情况,数据精度并没有丢失,我们再转换回来,即可得到原始的byte字节,然后在转换成字符串,这才是我们想要的。

有如下验证:

public class DecoderTest {
    public static void main(String[] args) {
        String name = "¥ᄐᅠ¦ᄌノ";
        char[] chars = name.toCharArray();
        byte[] bytes = new byte[chars.length];

        for(int i = 0; i < chars.length; i++){
            bytes[i] = (byte) chars[i];
        }

        System.out.println(new String(bytes));
    }
}

控制台输出结果为:张三

想法可行,那么,我们只要添加拦截器,在业务功能获取参数前,反转后存放到request中,就可以了。

添加如下拦截器:

import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Enumeration;

public class UndertowRevertInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        Enumeration parameterNames = request.getParameterNames();
        while (parameterNames.hasMoreElements()){
            String name = parameterNames.nextElement();
            String value = request.getParameter(name);
            value = revert(value);
            //注意不要和原有key重复,我不是教你写bug,只是提供一种思路。
            request.setAttribute(name,value); 
        }
        return true;
    }

    private String revert(String s){
        char[] chars = s.toCharArray();
        byte[] bytes = new byte[chars.length];

        for(int i = 0; i < chars.length; i++){
            bytes[i] = (byte) chars[i];
        }

        //如系统非使用UTF-8编码,请替换为带有编码格式的构造函数
        return new String(bytes);
    }
}

注意是放到了 attribute 里面,request不提供setParameter方法,想想也是合理的,http单次请求本来就是单向发送到后端的,setParameter做什么?

把拦截器注入到Spring当中:

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //注意把参数转换拦截器放到第一位,如果有多个拦截器,在其下面添加
        registry.addInterceptor(new UndertowRevertInterceptor());
    }
}

controller里面获取参数相应替换成:

String name = (String) request.getAttribute("name");

重启应用,得到正确的值,博主还拿了锟斤拷去测试:

锟斤拷被正确输出

七、其他办法

当然除了拦截器,还可以有如下方法:

  • 添加参数解析器 HandlerMethodArgumentResolver
  • 添加过滤器 ,可以考虑继承 OncePerRequestFilter ,参考 CharacterEncodingFilter 的实现。
  • 添加 aop,拦截点可以有很多,太麻烦,不建议。

大家有什么精巧的办法和别的想法,欢迎留言。

八、兼容性

博主因时间原因,并没有充分测试,只要undertow在参数转换的时候依然是由byte强转为char,本方法就会生效。

你可能感兴趣的:(springboot,后端,java,spring)