前言
"部署测试,部署预发布,一切测试就绪,上生产。
发布生产
闪退
What???
马上回滚
开始排查
后端一模一样的代码,不是 APP 端的问题吧。可APP 端没有发版啊。
…… 一番排查
原来是 APP 端打包,测试和预发布包 Header 传的都是Authorization,生产传的是authorization。就是大小写问题,那赶紧改。"
1
背景
首页接口只有登录才可以进入,因为首页要展示获取用户账户的一些信息。这里使用的是统一拦截,从Header 中获取 token 后,使用 token 获取用户信息。
而现在要改为用户未登录也可以查看首页信息中的宣传文案等等,只不过账户信息不显示。
原流程
整个过程代码在 ThreadLocal 底层原理 里面有所介绍。这里省略一部分代码。
@ComponentpublicclassTokenInterceptorimplementsHandlerInterceptor{@OverridepublicvoidafterCompletion(HttpServletRequestarg0,HttpServletResponsearg1,Objectarg2,Exceptionarg3)throwsException{LocalUserUtils.remove();}@OverridepublicbooleanpreHandle(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler)throwsException{//请求方法是否存在注解booleanassignableFrom=handler.getClass().isAssignableFrom(HandlerMethod.class);if(!assignableFrom){returntrue;}CheckTokencheckToken=null;if(handlerinstanceofHandlerMethod){checkToken=((HandlerMethod)handler).getMethodAnnotation(CheckToken.class);}//没有加注解直接放过if(checkToken==null){returntrue;}//从Header中获取AuthorizationStringauthorization=request.getHeader("Authorization");log.info("headerauthorization:{}",authorization);if(StringUtils.isBlank(authorization)){log.error("从Header中获取Authorization失败");throwCustomExceptionEnum.NOT_HAVE_TOKEN.throwCustomException();}//其他代码省略returntrue;}}
从代码中可以看出这里大概过程如下:
是使用拦截器拦截请求如果方法没有 CheckToken 注解直接放过有CheckToken 注解,则从 request 的header 中获取 Authorization
新需求
这里想到只需要把注解去掉,然后从请求参数中获取 token 即可。获取到走原逻辑,获取不到则只返回宣传文案等信息。
2
从Header 中获取信息
直接获取请求头某一个 headerName
@PostMapping("/getAuthorizationByKey")publicStringgetAuthorizationByKey(@RequestHeader("Authorization")Stringauthorization){log.info("获取Authorization--->{}",authorization);returnauthorization;}
使用 Map 获取所有请求头
@PostMapping("/getAuthorizationByMap")publicStringgetAuthorizationByMap(@RequestHeaderMapmap){Stringauthorization=map.get("Authorization");log.info("获取Authorization--->{}",authorization);returnauthorization;}
使用 MultiValueMap 获取请求头
@PostMapping("/getAuthorizationByMultiValueMap")publicStringgetAuthorizationByMultiValueMap(@RequestHeaderMultiValueMapmap){Listauthorization=map.get("Authorization");log.info("获取Authorization--->{}",authorization);return"SUCCESS";}
使用 HttpHeaders 获取请求头
@PostMapping("/getAuthorizationByHeaders")publicStringgetAuthorizationByHeaders(@RequestHeaderHttpHeadersheaders){Listauthorization=headers.get("Authorization");log.info("获取Authorization--->{}",authorization);return"SUCCESS";}
使用 HttpServletRequest 获取
@PostMapping("/getAuthorizationByServlet")publicStringgetAuthorizationByServlet(HttpServletRequestrequest){Stringauthorization=request.getHeader("Authorization");log.info("获取Authorization--->{}",authorization);returnauthorization;}
测试文件
经过测试这些都是可以的,最终选择使用 Map 接收 Header ,然后从 Map 中获取 Authorization。
"PS: 可能有小伙伴测试不过,发现接受的 header 里的 name 全都是小写了,可以继续阅读。
源码在文末,也可以关注公众号,发送 headerName/4 获取。"
你以为事情如果到这里就结束了,那真是太天真了。
这不,出现了文章开头的描述的场景,赶紧回滚,然后排查问题,最后定位到是 Header 的name 大小写问题。
思考
之前 APP 端也是这么传的,那为什么使用拦截器是正常的呢?上面的那几种方式是不是都是这样?不排除 tomcat 发现原来都会转换为小写,又是为什么?
3
模拟排查
环境配置
模拟生产首先使用相同的容器配置,这里排除了内置的 tomcat 容器,并且使用 undertow 容器。
org.springframework.bootspring-boot-starter-weborg.springframework.bootspring-boot-starter-loggingorg.springframework.bootspring-boot-starter-tomcatorg.springframework.bootspring-boot-starter-undertow
使用拦截器传小写为什么没有问题
修改使用小写 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 方法:
privatestaticinthigher(byteb){returnb&(b>='a'&&b