百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

作为开发:从 Header 获取数据这个坑你必须了解一下

wxin55 2024-10-27 15:57 22 浏览 0 评论


前言


" 部署测试,部署预发布,一切测试就绪,上生产。

发布生产

闪退

What???

马上回滚

开始排查

后端一模一样的代码,不是 APP 端的问题吧。可 APP 端没有发版啊。

…… 一番排查


原来是 APP 端打包,测试和预发布包 Header 的都是 Authorization ,生产传的是 authorization 。就是大小写问题,那赶紧改。 "


1

背景


首页接口只有登录才可以进入,因为首页要展示获取用户账户的一些信息。这里使用的是统一拦截,从 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;
    }
}


从代码中可以看出这里大概过程如下:

  1. 是使用拦截器拦截请求
  2. 如果方法没有 CheckToken 注解直接放过
  3. 有 CheckToken 注解,则从 request 的 header 中获取 Authorization


新需求

这里想到只需要把注解去掉,然后从请求参数中获取 token 即可。获取到走原逻辑,获取不到则只返回宣传文案等信息。


2

从 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 大小写问题。


思考

  1. 之前 APP 端也是这么传的,那为什么使用拦截器是正常的呢?
  2. 上面的那几种方式是不是都是这样?
  3. 不排除 tomcat 发现原来都会转换为小写,又是为什么?


3

模拟排查


环境配置

模拟生产首先使用相同的容器配置,这里排除了内置的 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);
}


这块的含义


  1. 如果 b 是小写字符则 b & 0xDF
  2. 如果 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 的时候,没有区分大小写进行获取。


4

容器对 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#getHeaderorg.apache.catalina.connector.Request#getHeaderorg.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 中获取相同。


5

总结


Q&A

Q: 为什么拦截器获取 Authorization 可以不区分大小写?

A: 从拦截器获取 Authorization 其实就是从 HttpServletRequest 中获取,这里无论使用 tomcat 还是使用 undertow 或者 jetty 获取 Header 都是忽略 headerName 的大小写的。具体可以阅读上面的源码分析。

Q: 这么多获取 Header 的方式有什么区别?

A: 不同的容器下实现方式不同,这里列表说明



通过表格发现,即使是不同的容器在使用 HttpHeaders 获取请求头是都是调用了 Spring 的 LinkedCaseInsensitiveMap 获取 header,并且内部忽略了大小写,这里比较推荐使用。


同样使用 HttpServletRequest 的方式获取也比较推荐。


结束语

本文主要是分析生产遇到的一个问题,然后开始探究原因,开始的时候发现是 Spring 的原因,因为使用 Map 接收时, headerName 什么格式就是什么格式。

在自己写 demo 时又发现,原来和 Spring 的关系并不大,是容器的原因。不同的容器处理方式不同。所以总结出来相关文章,供大家参考,不足之处,欢迎指正。


相关资料

[1] 本文源码地址:

https://github.com/liuzhihangs/header-demo

- <End /> -


作者:刘志航,一个宅宅的北漂程序员。


公众号:liuzhihangs,记录工作学习中的技术、开发及源码笔记;时不时分享一些生活中的见闻感悟。欢迎大佬来指导!

相关推荐

ES6中 Promise的使用场景?(es6promise用法例子)

一、介绍Promise,译为承诺,是异步编程的一种解决方案,比传统的解决方案(回调函数)更加合理和更加强大在以往我们如果处理多层异步操作,我们往往会像下面那样编写我们的代码doSomething(f...

JavaScript 对 Promise 并发的处理方法

Promise对象代表一个未来的值,它有三种状态:pending待定,这是Promise的初始状态,它可能成功,也可能失败,前途未卜fulfilled已完成,这是一种成功的状态,此时可以获取...

Promise的九大方法(promise的实例方法)

1、promise.resolv静态方法Promise.resolve(value)可以认为是newPromise方法的语法糖,比如Promise.resolve(42)可以认为是以下代码的语...

360前端一面~面试题解析(360前端开发面试题)

1.组件库按需加载怎么做的,具体打包配了什么-按需加载实现:借助打包工具(如Webpack的require.context或ES模块动态导入),在使用组件时才引入对应的代码。例如在V...

前端面试-Promise 的 finally 怎么实现的?如何在工作中使用?

Promise的finally方法是一个非常有用的工具,它无论Promise是成功(fulfilled)还是失败(rejected)都会执行,且不改变Promise的最终结果。它的实现原...

最简单手写Promise,30行代码理解Promise核心原理和发布订阅模式

看了全网手写Promise的,大部分对于新手还是比较难理解的,其中几个比较难的点:状态还未改变时通过发布订阅模式去收集事件实例化的时候通过调用构造函数里传出来的方法去修改类里面的状态,这个叫Re...

前端分享-Promise可以中途取消啦(promise可以取消吗)

传统Promise就像一台需要手动组装的设备,每次使用都要重新接线。而Promise.withResolvers的出现,相当于给开发者发了一个智能遥控器,可以随时随地控制异步操作。它解决了三大...

手写 Promise(手写输入法 中文)

前言都2020年了,Promise大家肯定都在用了,但是估计很多人对其原理还是一知半解,今天就让我们一起实现一个符合PromiseA+规范的Promise。附PromiseA+规范地址...

什么是 Promise.allSettled()!新手老手都要会?

Promise.allSettled()方法返回一个在所有给定的promise都已经fulfilled或rejected后的promise,并带有一个对象数组,每个对象表示对应的pr...

前端面试-关于Promise解析与高频面试题示范

Promise是啥,直接上图:Promise就是处理异步函数的API,它可以包裹一个异步函数,在异步函数完成时抛出完成状态,让代码结束远古时无限回掉的窘境。配合async/await语法糖,可...

宇宙厂:为什么前端离不开 Promise.withResolvers() ?

大家好,很高兴又见面了,我是"高级前端进阶",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发。1.为什么需要Promise.with...

Promise 新增了一个超实用的 API!

在JavaScript的世界里,Promise一直是处理异步操作的神器。而现在,随着ES2025的发布,Promise又迎来了一个超实用的新成员——Promise.try()!这个新方法简...

一次搞懂 Promise 异步处理(promise 异步顺序执行)

PromisePromise就像这个词的表面意识一样,表示一种承诺、许诺,会在后面给出一个结果,成功或者失败。现在已经成为了主流的异步编程的操作方式,写进了标准里面。状态Promise有且仅有...

Promise 核心机制详解(promise机制的实现原理)

一、Promise的核心状态机Promise本质上是一个状态机,其行为由内部状态严格管控。每个Promise实例在创建时处于Pending(等待)状态,此时异步操作尚未完成。当异步操作成功...

javascript——Promise(js实现promise)

1.PromiseES6开始支持,Promise对象用于一个异步操作的最终完成(包括成功和失败)及结果值的表示。简单说就是处理异步请求的。之所以叫Promise,就是我承诺,如果成功则怎么处理,失败怎...

取消回复欢迎 发表评论: