共计 7848 个字符,预计需要花费 20 分钟才能阅读完成。
前言
部署测试,部署预公布,所有测试就绪,上生产。
公布生产
闪退
What???
马上回滚
开始排查
后端截然不同的代码,不是 APP 端的问题吧。可 APP 端没有发版啊。
…… 一番排查
原来是 APP 端打包,测试和预公布包 Header 传的都是
Authorization
,生产传的是authorization
。就是大小写问题,那连忙改。公众号:『刘志航』,记录工作学习中的技术、开发及源码笔记;时不时分享一些生存中的见闻感悟。欢送大佬来领导!
背景
首页接口只有登录才能够进入,因为首页要展现获取用户账户的一些信息。这里应用的是对立拦挡,从 Header 中获取 token 后,应用 token 获取用户信息。
而当初要改为用户未登录也能够查看首页信息中的宣传文案等等,只不过账户信息不显示。
原流程
整个过程代码在 ThreadLocal 底层原理 外面有所介绍。这里省略一部分代码。
@Component
public class TokenInterceptor implements HandlerInterceptor {
@Override
public void afterCompletion(HttpServletRequest arg0, HttpServletResponse arg1, Object arg2, Exception arg3)
throws Exception {LocalUserUtils.remove();
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 申请办法是否存在注解
boolean assignableFrom = handler.getClass().isAssignableFrom(HandlerMethod.class);
if (!assignableFrom) {return true;}
CheckToken checkToken = null;
if (handler instanceof HandlerMethod) {checkToken = ((HandlerMethod) handler).getMethodAnnotation(CheckToken.class);
}
// 没有加注解 间接放过
if (checkToken == null) {return true;}
// 从 Header 中获取 Authorization
String authorization = request.getHeader("Authorization");
log.info("header authorization : {}", authorization);
if (StringUtils.isBlank(authorization)) {log.error("从 Header 中获取 Authorization 失败");
throw CustomExceptionEnum.NOT_HAVE_TOKEN.throwCustomException();}
// 其余代码省略
return true;
}
}
从代码中能够看出这里大略过程如下:
- 是应用拦截器拦挡申请
- 如果办法没有 CheckToken 注解间接放过
- 有 CheckToken 注解,则从 request 的 header 中获取 Authorization
新需要
这里想到只须要把注解去掉,而后从申请参数中获取 token 即可。获取到走原逻辑,获取不到则只返回宣传文案等信息。
从 Header 中获取信息
间接获取申请头某一个 headerName
@PostMapping("/getAuthorizationByKey")
public String getAuthorizationByKey(@RequestHeader("Authorization") String authorization) {log.info("获取 Authorization --->{}", authorization);
return authorization;
}
应用 Map 获取所有申请头
@PostMapping("/getAuthorizationByMap")
public String getAuthorizationByMap(@RequestHeader Map<String, String> map) {String authorization = map.get("Authorization");
log.info("获取 Authorization --->{}", authorization);
return authorization;
}
应用 MultiValueMap 获取申请头
@PostMapping("/getAuthorizationByMultiValueMap")
public String getAuthorizationByMultiValueMap(@RequestHeader MultiValueMap<String, String> map) {List<String> authorization = map.get("Authorization");
log.info("获取 Authorization --->{}", authorization);
return "SUCCESS";
}
应用 HttpHeaders 获取申请头
@PostMapping("/getAuthorizationByHeaders")
public String getAuthorizationByHeaders(@RequestHeader HttpHeaders headers) {List<String> authorization = headers.get("Authorization");
log.info("获取 Authorization --->{}", authorization);
return "SUCCESS";
}
应用 HttpServletRequest 获取
@PostMapping("/getAuthorizationByServlet")
public String getAuthorizationByServlet(HttpServletRequest request) {String authorization = request.getHeader("Authorization");
log.info("获取 Authorization --->{}", authorization);
return authorization;
}
测试文件
通过测试这些都是能够的,最终抉择应用 Map 接管 Header,而后从 Map 中获取 Authorization。
PS: 可能有小伙伴测试不过,发现承受的 header 里的 name 全都是小写了,能够持续浏览。
源码在文末,也能够关注公众号,发送 headerName/4 获取。
你认为事件如果到这里就完结了,那真是太天真了。
这不,呈现了文章结尾的形容的场景,连忙回滚,而后排查问题,最初定位到是 Header 的 name 大小写问题。
思考
- 之前 APP 端也是这么传的,那为什么应用拦截器是失常的呢?
- 下面的那几种形式是不是都是这样?
- 不排除 tomcat 发现原来都会转换为小写,又是为什么?
模仿排查
环境配置
模仿生产首先应用雷同的容器配置,这里排除了内置的 tomcat 容器,并且应用 undertow 容器。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
<!-- Exclude the Tomcat dependency -->
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
应用拦截器传小写为什么没有问题
- 批改应用小写
authorization
- debug 断点
神奇的一幕呈现了,收到的的确是小写,然而 request.getHeader(“Authorization”); 却能够获取到 authorization
- F7 持续往里跟
io.undertow.servlet.spec.HttpServletRequestImpl#getHeader
第 190 行,从 HeaderMap 中获取第一个元素
io.undertow.util.HeaderMap#getFirst
第 297 行,通过 getEntry 办法获取 header
持续追踪,发现在 io.undertow.util.HeaderMap#getEntry(java.lang.String)
办法 77~79 行的时候获取到了 header 信息。那就看一下这块的源码吧。
在认真看一下发现是 77 行 final int hc = HttpString.hashCodeOf(headerName);
在获取 name 的 hashCode 时,这里无论大小写,都是同一个 hashCode。这块代码如下
higher 办法:
private static int higher(byte b) {return b & (b >= 'a' && b <= 'z' ? 0xDF : 0xFF);
}
这块的含意
- 如果 b 是小写字符则
b & 0xDF
- 如果 b 是大写字符则
b & 0xFF
对照 ASCII 表,大小写字母相差 32 而 0xFF(255) 和 0xDF(223) 同样相差 32,所以问题定位到了。header 的 name 无论是大写还是小写,都会查出同一个值。
当然你也能够这么传
这样的话谁在下面,Header 中应用的 name 就是那个。
应用 Map 为什么会辨别大小写
传入的是大写
HttpServlet
-> DispatcherServlet#doDispatch
-> AbstractHandlerMethodAdapter#handle
-> RequestMappingHandlerAdapter#handleInternal
-> RequestMappingHandlerAdapter#invokeHandlerMethod
-> ServletInvocableHandlerMethod#invokeAndHandle
-> InvocableHandlerMethod#invokeForRequest (解析参数值)
-> InvocableHandlerMethod#getMethodArgumentValues
-> RequestHeaderMapMethodArgumentResolver#resolveArgument
如图所示 String headerName = iterator.next();
name 被辨别大小写放到了 LinkedHashMap 中,后续会反射调用对应的 Controller 办法。
所以也就呈现了我所遇到的问题。
当然实践上 APP 客户端不应该测试和预公布应用大写,而生产应用小写。
下面浏览的源码只是 Spring 对 Header 的解决,Spring 在 HttpServlet
收到申请时,Spring 没有对申请 Header 的 name 大小写进行转换,只是在获取对应 value 的时候,没有辨别大小写进行获取。
容器对 header 的解决
undertow 容器的解决
- 申请参数的解决
这里发现 undertow 并没有对申请参数进行大小写转换解决操作。
- 从 HttpServletRequest 获取 header
debug 发现调用的是 io.undertow.servlet.spec.HttpServletRequestImpl#getHeader
,这个过程就是下面的排查过程。
- 从 Headers 中获取 header
通过 debug 发现 jetty 调用的是 org.springframework.http.HttpHeaders#get
,而后调用 org.springframework.util.MultiValueMapAdapter#get
,而后调用 org.springframework.util.LinkedCaseInsensitiveMap#get
这里会不辨别大小写
- 从 MultiValueMap 获取 header
这块 debug 发现是间接从 LinkedHashMap
获取的,所以辨别了大小写。
tomcat 容器的解决
- 申请参数的解决
而如果没有排除的话,即应用内嵌的 tomcat 容器无论传递大写还是小写,接管到的全部都是小写,又是怎么个状况呢?
通过 debug 发现没有排除 tomcat 应用的是,在接管申请时应用的是 org.apache.coyote.http11.Http11Processor
。
在 Http11Processor#service
办法中
类 284 行负责解决解析 header
进入 org.apache.coyote.http11.Http11InputBuffer#parseHeaders
办法
第 589 行(Download Sources 后),浏览 parseHeader
办法
发现会将申请 header 的 name 转换为小写
- 从 HttpServletRequest 获取 header
当应用 tomcat 容器时,调用 org.apache.catalina.connector.RequestFacade#getHeader
,org.apache.catalina.connector.Request#getHeader
,org.apache.coyote.Request#getHeader
org.apache.tomcat.util.http.MimeHeaders#getHeader
最初调用 org.apache.tomcat.util.http.MimeHeaders#getValue
获取 header
这里也会疏忽大小写判断
- 从 Headers 获取 header
通过 debug 发现 tomcat 容器下调用的是 org.springframework.http.HttpHeaders#get
,而后调用 org.springframework.util.MultiValueMapAdapter#get
,而后调用 org.springframework.util.LinkedCaseInsensitiveMap#get
这里会不辨别大小写
- 从 MultiValueMap 获取 header
这块 debug 发现是间接从 LinkedHashMap
获取的,所以辨别了大小写。
jetty 容器的解决
- 申请参数的解决
如果换成 jetty 容器的话
在 org.eclipse.jetty.server.HttpConnection
中又会发现无论传入大写还是小写都会被转换为驼峰。
源码能够浏览 org.eclipse.jetty.http.HttpParser#parseFields
会转换为驼峰命名法。
- 从 HttpServletRequest 获取 header
通过 debug 发现 jetty 调用的是 org.eclipse.jetty.server.Request#getHeader
jetty 在获取 header 时,会调用 org.eclipse.jetty.http.HttpFields#get
原来在获取的时候疏忽了大小写
- 从 Headers 获取 header
通过 debug 发现 jetty 容器下调用的是 org.springframework.http.HttpHeaders#get
,而后调用 org.springframework.util.MultiValueMapAdapter#get
,而后调用 org.springframework.util.LinkedCaseInsensitiveMap#get
这里会不辨别大小写
- 从 MultiValueMap 获取
也是调用的 org.springframework.util.MultiValueMapAdapter#get
而后不辨别大小写。和从 Headers 中获取雷同。
总结
Q&A
Q: 为什么拦截器获取 Authorization 能够不辨别大小写?
A: 从拦截器获取 Authorization 其实就是从 HttpServletRequest
中获取,这里无论应用 tomcat 还是应用 undertow 或者 jetty 获取 Header 是都是疏忽 headerName 的大小写的。具体能够浏览下面的源码剖析。
Q: 这么多获取 Header 的形式有什么区别?
A:
不同的容器下实现形式不同,这里列表阐明
undertow | tomcat | jetty | |
---|---|---|---|
申请参数大小写转换 | 不变 | 小写 | 驼峰 |
间接获取申请头某一个 headerName | 疏忽大小写,不能为空 | 疏忽大小写,不能为空 | 疏忽大小写,不能为空 |
应用 Map 获取所有申请头 | Map 的 key 和传入 headerName 大小写的统一,保持一致可获取到 | Map 的 key 全是小写,须要应用小写 headerName 获取 | Map 的 key 是驼峰命名法,要应用驼峰命名才能够获取到 |
应用 MultiValueMap 获取申请头 | 理论是从 LinkedHashMap 中获取,辨别大小写 | 理论是从 LinkedHashMap 中获取,辨别大小写 | 从 LinkedCaseInsensitiveMap 获取,不辨别大小写 |
应用 HttpHeaders 获取申请头 | 从 LinkedCaseInsensitiveMap 获取,不辨别大小写 | 从 LinkedCaseInsensitiveMap 获取,不辨别大小写 | 从 LinkedCaseInsensitiveMap 获取,不辨别大小写 |
应用 HttpServletRequest 获取 | 应用 HttpString.hashCodeOf(headerName) 疏忽了大小写 | 调用 MimeHeaders#getValue 疏忽了大小写 | HttpFields#get 疏忽了大小写 |
通过表格发现,即便是不同的容器在应用 HttpHeaders 获取申请头是都是调用了 Spring 的 LinkedCaseInsensitiveMap
获取 header,并且外部疏忽了大小写,这里比拟举荐应用。
同样应用 HttpServletRequest 的形式获取也比拟举荐。
结束语
本文次要是剖析生产遇到的一个问题,而后开始探索起因,开始的时候发现是 Spring 的起因,因为应用 Map 接管时,headerName 什么格局就是什么格局。
在本人写 demo 时又发现,原来和 Spring 的关系并不大,是容器的起因。不同的容器解决形式不同。所以总结进去相干文章,供大家参考,不足之处,欢送斧正。
相干材料
- 本文源码地址:https://github.com/liuzhihang…