当前位置:网站首页>App crashed inexplicably. At first, it thought it was the case of the name in the header. Finally, it was found that it was the fault of the container!

App crashed inexplicably. At first, it thought it was the case of the name in the header. Finally, it was found that it was the fault of the container!

2020-11-08 23:46:04 Liu Zhihang

Preface


The deployment of test , Deployment pre release , Everything is ready for testing , Go to production .

Release production

Flash back

What???

Roll back now

Start troubleshooting

Code as like as two peas at the back end. , No APP Let's talk about it . can APP There's no release on the end .

…… An investigation

Turned out to be APP End pack , Test and pre release packages Header It's all over the place Authorization , What's going on in production authorization . It's case , Change it quickly .


official account :『 Liu Zhihang 』, Record the skills in work study 、 Development and source notes ; From time to time to share some of the life experience . You are welcome to guide !

background

The homepage interface can only be accessed by login , Because the home page to show access to user accounts some information . Here, we use unified interception , from Header In order to get token after , Use token Get user information .

And now to change to users who are not logged in, you can also view the propaganda copy in the home page information and so on , Only the account information is not displayed .

Original process

FiwOie-8ZLjgv

The whole process is in the code ThreadLocal Underlying principle There is an introduction in it . Part of the code is omitted here .

@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 {

        //  Does the request method have a comment 
        boolean assignableFrom = handler.getClass().isAssignableFrom(HandlerMethod.class);

        if (!assignableFrom) {
            return true;
        }

        CheckToken checkToken = null;
        if (handler instanceof HandlerMethod) {
            checkToken = ((HandlerMethod) handler).getMethodAnnotation(CheckToken.class);
        }

        //  There is no comment   Let it go 
        if (checkToken == null) {
            return true;
        }

        //  from Header In order to get Authorization
        String authorization = request.getHeader("Authorization");
        log.info("header authorization : {}", authorization);
        if (StringUtils.isBlank(authorization)) {
            log.error(" from Header In order to get Authorization Failure ");
            throw CustomExceptionEnum.NOT_HAVE_TOKEN.throwCustomException();
        }

        //  Other code omissions 

        return true;
    }
}

From the code, we can see that the general process here is as follows :

  1. It's using interceptors to intercept requests
  2. If the method does not CheckToken Let's go of the notes
  3. Yes CheckToken annotation , From request Of header In order to get Authorization

The new demand

I just want to get rid of the annotations , It is then retrieved from the request parameters token that will do . Get the original logic , If you can't get it, you will only return the information such as propaganda copy .

from Header For information

Get a request header directly headerName

@PostMapping("/getAuthorizationByKey")
public String getAuthorizationByKey(@RequestHeader("Authorization") String authorization) {

    log.info(" obtain  Authorization --->{}", authorization);

    return authorization;
}

Use Map Get all request headers

@PostMapping("/getAuthorizationByMap")
public String getAuthorizationByMap(@RequestHeader Map<String, String> map) {

    String authorization = map.get("Authorization");

    log.info(" obtain  Authorization --->{}", authorization);

    return authorization;
}

Use MultiValueMap Get request header

@PostMapping("/getAuthorizationByMultiValueMap")
public String getAuthorizationByMultiValueMap(@RequestHeader MultiValueMap<String, String> map) {

    List<String> authorization = map.get("Authorization");

    log.info(" obtain  Authorization --->{}", authorization);

    return "SUCCESS";
}

Use HttpHeaders Get request header

@PostMapping("/getAuthorizationByHeaders")
public String getAuthorizationByHeaders(@RequestHeader HttpHeaders headers) {

    List<String> authorization = headers.get("Authorization");

    log.info(" obtain  Authorization --->{}", authorization);

    return "SUCCESS";
}

Use HttpServletRequest obtain

@PostMapping("/getAuthorizationByServlet")
public String getAuthorizationByServlet(HttpServletRequest request) {

    String authorization = request.getHeader("Authorization");

    log.info(" obtain  Authorization --->{}", authorization);

    return authorization;
}

The test file

dqAEES-LSudFy

After testing, these are OK , Finally choose to use Map receive Header , And then from Map In order to get Authorization.

PS: There may be small partners testing, but , Find accepted header Inside name It's all lowercase , Can continue reading .
The source code is at the end of the article , You can also pay attention to the official account , send out headerName/4 obtain .

rXC2fS-cVftVR

You think it's over if you get here , That's naive .

see , The scene described at the beginning of the article appears , Roll back quickly , Then check the problem , Finally, it was determined to be yes Header Of name Case problem .

reflection

  1. Before APP That's what Duan said , So why is it normal to use interceptors ?
  2. Are all the above ways the same ?
  3. Do not rule out tomcat Found that the original will be converted to lowercase , And why ?

Simulation investigation

Environment configuration

Simulation production first uses the same container configuration , The built-in tomcat Containers , And use undertow Containers .

<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>

Why not use interceptors to pass lowercase

  • Use lowercase for modification authorization

BnwZnf-7AvYYg

  • debug The breakpoint

OOjOyV-54K6Na

A magical scene appeared , It's actually in lowercase , however request.getHeader("Authorization"); But you can get it authorization

  • F7 Keep following

JwgnFS-cW3pJY

io.undertow.servlet.spec.HttpServletRequestImpl#getHeader The first 190 That's ok , from HeaderMap Gets the first element in the

DnRSum-crs0Mb

io.undertow.util.HeaderMap#getFirst The first 297 That's ok , adopt getEntry Method to get header

C8lSGw-XxKDPW

Keep tracking , Found in io.undertow.util.HeaderMap#getEntry(java.lang.String) Method 77~79 We got it when we got it header Information . Then take a look at this source code .

On a closer look, it turns out that 77 That's ok final int hc = HttpString.hashCodeOf(headerName); In obtaining name Of hashCode when , It doesn't matter whether it's case or not , It's all the same hashCode. The code is as follows

3v1FVJ-MuPXUA

higher Method :

private static int higher(byte b) {
    return b & (b >= 'a' && b <= 'z' ? 0xDF : 0xFF);
}

What does this mean

  1. If b Is a lowercase character b & 0xDF
  2. If b Is a capital character, then b & 0xFF

contrast ASCII surface , Difference between upper and lower case letters 32 and 0xFF(255) and 0xDF(223) The same difference 32, So the problem is fixed .header Of name Whether it's uppercase or lowercase , Will find the same value .

Of course you can do the same

VOOF8k-LoGjAf

So who's up there ,Header Used in name That's it. .

Use Map Why is case sensitive

Uppercase is passed in

HttpServlet 
-> DispatcherServlet#doDispatch 
-> AbstractHandlerMethodAdapter#handle 
-> RequestMappingHandlerAdapter#handleInternal 
-> RequestMappingHandlerAdapter#invokeHandlerMethod 
-> ServletInvocableHandlerMethod#invokeAndHandle
-> InvocableHandlerMethod#invokeForRequest ( Parse parameter values )
-> InvocableHandlerMethod#getMethodArgumentValues
-> RequestHeaderMapMethodArgumentResolver#resolveArgument

dqAEES-LSudFy

As shown in the figure String headerName = iterator.next(); name Case sensitive LinkedHashMap in , The corresponding Controller Method .

So there's the problem I've had .

Of course, in theory APP Clients should not test and pre release with uppercase , Production uses lowercase .

The source code read above is just Spring Yes Header To deal with ,Spring stay HttpServlet On request ,Spring No request for Header Of name Case conversion , Just getting the corresponding value When , No case sensitive fetch .

Container pair header To deal with

undertow Handling of containers
  • Processing of request parameters

Here we find undertow There is no case conversion operation on the request parameters .

  • from HttpServletRequest obtain header

debug It was found that io.undertow.servlet.spec.HttpServletRequestImpl#getHeader, This process is the above investigation process .

  • from Headers In order to get header

adopt debug Find out jetty It's called org.springframework.http.HttpHeaders#get, And then call org.springframework.util.MultiValueMapAdapter#get, And then call org.springframework.util.LinkedCaseInsensitiveMap#get

nRw6xN-GkUqAN

Case insensitive

  • from MultiValueMap obtain header

This piece of debug The discovery was made directly from LinkedHashMap Acquired , So it's case sensitive .

tomcat Handling of containers

  • Processing of request parameters

And if not excluded , The embedded tomcat The container passes both uppercase and lowercase , All received are lowercase , What is the situation ?

adopt debug It was found that it was not excluded tomcat It uses , When receiving a request, the org.apache.coyote.http11.Http11Processor.

stay Http11Processor#service In the method

303mmp-6TczCR

class 284 The bank is responsible for processing the analysis header

Get into org.apache.coyote.http11.Http11InputBuffer#parseHeaders Method

cqHTEZ-1E9mH7

The first 589 That's ok (Download Sources after ), read parseHeader Method

oEEbej-rMulR7

Discovery will request header Of name Convert to lowercase

  • from HttpServletRequest obtain header

When using tomcat When the container , call org.apache.catalina.connector.RequestFacade#getHeader, org.apache.catalina.connector.Request#getHeader, org.apache.coyote.Request#getHeader org.apache.tomcat.util.http.MimeHeaders#getHeader Last call org.apache.tomcat.util.http.MimeHeaders#getValue obtain header

qDJXXn-z6hTjZ

Case judgment is also ignored here

  • from Headers obtain header

adopt debug Find out tomcat What the container calls down is org.springframework.http.HttpHeaders#get, And then call org.springframework.util.MultiValueMapAdapter#get, And then call org.springframework.util.LinkedCaseInsensitiveMap#get

nRw6xN-GkUqAN

Case insensitive

  • from MultiValueMap obtain header

This piece of debug The discovery was made directly from LinkedHashMap Acquired , So it's case sensitive .

jetty Handling of containers

  • Processing of request parameters

If replaced jetty If the container

stay org.eclipse.jetty.server.HttpConnection You will find that both uppercase and lowercase are converted to humps .

Source code can be read org.eclipse.jetty.http.HttpParser#parseFields

Q9KjmT-HSRusz

Will be converted to hump nomenclature .

  • from HttpServletRequest obtain header

adopt debug Find out jetty It's called org.eclipse.jetty.server.Request#getHeader

H9onCc-uyaMMn

jetty In obtaining header when , Would call org.eclipse.jetty.http.HttpFields#get

BOkxFw-op6ZIV

ioe7pS-uNuRU6

It turns out that the case is ignored in the acquisition

  • from Headers obtain header

adopt debug Find out jetty What the container calls down is org.springframework.http.HttpHeaders#get, And then call org.springframework.util.MultiValueMapAdapter#get, And then call org.springframework.util.LinkedCaseInsensitiveMap#get

nRw6xN-GkUqAN

Case insensitive

  • from MultiValueMap obtain

Also called org.springframework.util.MultiValueMapAdapter#get Then it is case insensitive . And from the Headers Get the same in .

summary

Q&A

Q: Why interceptors get Authorization Can be case insensitive ?

A: Get from interceptor Authorization In fact, from HttpServletRequest In order to get , No matter what you use here tomcat Or use undertow perhaps jetty obtain Header Yes, they are all ignored headerName Upper and lower case . Specific can read the above source analysis .

Q: So much to get Header What's the difference in the way you do it ?
A:

Different containers can be implemented in different ways , Here is a list

undertow tomcat jetty
Request parameter case conversion unchanged A lowercase letter hump
Get a request header directly headerName Ignore case , Can't be empty Ignore case , Can't be empty Ignore case , Can't be empty
Use Map Get all request headers Map Of key And incoming headerName Case consistency , Consistent access Map Of key It's all lowercase , Lowercase is required headerName obtain Map Of key It's the hump nomenclature , Only by naming humps can we get
Use MultiValueMap Get request header It's actually from LinkedHashMap In order to get , Case sensitive It's actually from LinkedHashMap In order to get , Case sensitive from LinkedCaseInsensitiveMap obtain , Case insensitive
Use HttpHeaders Get request header from LinkedCaseInsensitiveMap obtain , Case insensitive from LinkedCaseInsensitiveMap obtain , Case insensitive from LinkedCaseInsensitiveMap obtain , Case insensitive
Use HttpServletRequest obtain Use HttpString.hashCodeOf(headerName) Case ignored call MimeHeaders#getValue Case ignored HttpFields#get Case ignored

Find through the table , Even if different containers are in use HttpHeaders Get the request header is called Spring Of LinkedCaseInsensitiveMap obtain header, And case is ignored internally , It is recommended here .

Also use HttpServletRequest It's also recommended to get it .

Conclusion

This paper mainly analyzes a problem in production , And then you start to explore why , At the beginning, I found that it was Spring Why , Because use Map Reception time , headerName What is the format .

Write on your own demo I found that , Original and Spring It doesn't matter much , It's the container . Different containers are handled in different ways . So summarize the relevant articles , For your reference , deficiencies , Welcome to correct .

  1. This article source address :https://github.com/liuzhihang/header-demo

版权声明
本文为[Liu Zhihang]所创,转载请带上原文链接,感谢