谁动了我的编解码?

编解码问题的深入探究(基于 Tomcat 的 SpringMVC)

前置知识

URI和URL

在常规意义上,URI是URL的超集,即URL是URI的一种,常规意义上的URI包含URL和URN

URI(Uniform Resource Identifier,统一资源标识符)

URL(Uniform Resource Locator,统一资源定位符),如:https://example.com/index.html?name=zhangsanhttp://127.0.0.1:8099/index.html?name=zhangsan

URN(Uniform Resource Name,统一资源名称)如:urn:isbn:0451450523

一个典型的请求行,由 method path[?queryString] protocol 组成(即方法、URI 和 HTTP 版本),如:GET /index.html?name=zhangsan HTTP/1.1

URI指的是 path[?queryString] 这一部分,queryString 是 GET 请求的参数,如:/index.html?name=zhangsan

即请求行中的 URI 会包含 GET 请求的参数,但不会包含 POST 请求的参数

为什么请求行中的 URI 不包含 https://example.com 这部分呢?

因为这部分在客户端和服务端通过TCP建立连接时已经确定,后续真正发送请求自然不必再包含(可以减少请求的字节总数)

在 Servlet 层面,即由 Tomcat 等 Servlet 容器创建的 HttpServletRequest 这一请求对象就是原始请求的封装,其对 URI 和 URL 的定义存在不同

请求对象中的 URI 指的是 path 部分,即不包含请求参数,如:/index.html

请求对象中的 URL 指的是 https://example.com/index.html,同样不包含请求参数

GET 和 POST

GET 请求会把请求参数直接添加到请求行的 URI 中,也会直接显示在浏览器的地址栏,甚至是在保存在浏览器历史、保存在服务器日志等,这增加了信息泄露的风险,即使采用 HTTPS 也无法解决(HTTPS解决的是请求在网络传输过程中的信息保护)

POST 方法则会把请求参数放到请求体中,浏览器历史中不会保存,服务器如果不刻意操作也不会保存,大大减少了信息泄露的风险

Tomcat 工作流程

客户发送 HTTP 请求到达 Tomcat 后:

Tomcat 首先会新建 HttpServletRequest 和 HttpServletResponse 这两个对象

然后 Tomcat 会对请求行(里面包含了 Mehtod、URI、Protocol)和请求头(里面包含了诸如Cookie、Accept、User-Agent等元数据)进行解码并填充进 HttpServletRequest(注意此时不解码请求体)

其次 Tomcat 会通过 HttpServletRequest 获取路径,查询是否有匹配的Filter,有则依次调用 Filter,否则跳过

接下来,Tomcat 通过 HttpServletRequest 获取路径,基于配置的映射规则,查询是否有匹配的 Servlet,如果匹配,执行 Servlet(如果使用了 SpringMVC 框架,则只存在一个 DispatcherServlet,接收所有所需的 URL,然后进行内部分发) 后将 HttpServletResponse 返回给客户

如果不匹配,Tomcat 通过 HttpServletRequest 获取路径,基于 DefaultServlet 这个内置的 Servlet,有则返回静态资源

如果仍没有静态资源匹配,DefaultServlet 会在 HttpServletResponse 的状态行 填入 404 状态码及其他相关信息

最后,再次调用对应的 Filter,然后 Tomcat 将 HttpServletResponse 返回给客户

注意1:如果自定义了 DefaultServlet 的映射路径(默认其匹配任意路径),则又存在一种可能,即 路径连 DefaultServlet 都不匹配,此时会在组件层面对 HttpServletResponse 的状态行 填入 404 状态码及其他相关信息

注意2:如果配置了 404 error-page 则无论 DefaultServlet 还是 组件 都会把 404 错误转交给 error-page(转交过程由 ErrorDispatcher 这个组件完成)

编解码的设置

请求或响应的各个部分的解码一旦完成对应的字符就会保存下来,如果要重写解码则需要先将字符重新按解码方式编码为字节,再按照新的解码方式对字节进行解码

这个过程比较绕,但很重要

请求的解码

在 Tomcat 封装 HttpServletRequest 时已经完成了对 请求行和请求头的解码工作,而该解码方式是在 Tomcat 的配置文件中配置的

见CATALINA_HOME/conf/server.xml

CATALINA_HOME为 Tomcat 的安装目录

<Connector port="8099" protocol="HTTP/1.1"
           connectionTimeout="20000"
           redirectPort="8443"
           maxParameterCount="1000"
           URIEncoding="UTF-8"
           />

GET 请求的解码

GET 请求的参数会直接存在请求行中,也就是在 Tomcat 层面,已经对 GET 请求的参数进行了解析,所以首先应确保配置文件的解码格式正确(什么叫正确?表单页面的编码格式和配置文件的解码格式一致就视为正确),这基本可以解决 GET 请求的乱码问题

POST 请求的解码

POST 请求的参数存在请求体,如前所述,Tomcat 在容器层面并未对请求体进行解码,因此在通过 getParameter() 方法获取之前其都还是字节,然而一旦进行了获取,如果未显式设置解码格式,这一操作本身就会按照 Tomcat 默认的编解码格式(ISO-8859-1)进行解码,现在通常都是基于 UTF-8 进行编解码,这一默认编解码格式显然极易造成乱码,解决办法就是在进行参数获取前先通过 setCharacterEncoding() 方法进行编解码格式的设置(实践中推荐总是把其作为首个操作),这有时可以解决乱码问题

如果使用 SpringMVC 这一框架,往往出现即使在控制器方法中设置编解码格式也依然乱码的情况,这是为什么?

还记得前面提到的一旦解码发生,字节到字符的过程就已经完成了吗?

是的,实际上,如果你在控制器的方法参数中想直接获取到请求参数时,就会造成请求体被提前解码,这是 SpringMVC 框架自动完成的

框架在通过相关流程获取到控制器方法后会进行方法参数的解析,并根据一套复杂的规则进行参数匹配,然后填充参数调用方法,这也是你可以直接在方法中直接使用HttpServletRequest、HttpServletResponse、甚至是通过 String 等直接获取到请求的参数的原因

于是,新的问题产生了,为什么可以直接在方法参数中指定方法参数并匹配到对应的请求参数?

显然,在此过程中 SpringMVC 已经进行过请求体的解码了,不然怎么能获取到 POST 请求的参数值呢?不幸的是 SpringMVC 解码依然使用的 ISO-8859-1 这一默认格式

现在明白为什么还是乱码了,最简单粗暴的解决办法是不在方法参数中获取请求参数,而是在方法内先设置编解码格式,再通过 HttpServletRequest 来获取,这很有效,可自行尝试验证;但这完全浪费了 SpringMVC 框架提供的 请求参数自动注入控制器方法 这一重要功能,因此还是需要另一种通用的办法

Filter 正是解决方案的最佳选择,它发生在 SpringMVC 工作流程之前(准确说是 DispatcherServlet 这一核心 Servlet 之前),只要在 Filter 中提前设置请求的编解码格式即可确保后续 SpringMVC 中使用的是正确的编解码格式,现在终于基本解决乱码问题了

为什么不是肯定解决,因为实际开发中,还可能存在其他的过滤器、或拦截器、AOP等组件进行其他设置,你永远无法保证你的编解码设置没有被其他组件覆盖掉,你能做的就是在乱码依然发生时,一步步排查,找到那个改动你编解码设置的罪魁祸首,进行友好沟通、亲切交流!

响应的编码

响应的编码问题没有太多可讲的,远没有请求的解码来得复杂,你需要做的就是确保响应的编码格式是客户端可识别的,并且确保和请求的解码格式一致

总结

前文备述,无需总结,但记住:

你总是应该保证项目的编解码格式是统一的!!!

关于 SpringMVC的工作流程细节此处不进行讲解,这也是一个复杂的话题

事实上,Tomcat 的工作流程细节也是一个复杂的话题(这当然也绕不开 Servlet),此处已经假定你有所了解

如果你不了解以上两者,这部分内容对你的意义大打折扣,因为这并未直接给代码方案,而是窥探了问题的核心,如果搞明白了问题的核心,代码方案就是 CTRL+C/V

#java##spring##spring boot##tomcat##编解码#

本专栏不讲解基础,随机更新话题,记录更加接近底层原理的相关知识 基础见https://www.nowcoder.com/creation/manager/columnDetail/04yp33

全部评论

相关推荐

评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务