4

架构师技能8:springboot全局handler处理http 404错误引发登录失效的问题

 1 year ago
source link: https://blog.csdn.net/hguisu/article/details/128140311
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

开篇语录:以架构师的能力标准去分析每个问题,过后由表及里分析问题的本质,复盘总结经验,并把总结内容记录下来。当你解决各种各样的问题,也就积累了丰富的解决问题的经验,解决问题的能力也将自然得到极大的提升。

励志做架构师的撸码人,认知很重要,可以订阅:架构设计专栏


国庆前我们线上出现一次故障:用户无法登录某个微服务,后面一段时间后就自动恢复了,然后我持续跟踪和分析这个问题好久找到原因,顺便在此记录下来。

二、问题定位


出现这种问题,肯定是需要通过日志来定位,由于业务服务日志没有异常日常,因此通过外围日志来分析:

1、通过nginx日志检查http的请求异常信息:

在接入层的其中一台nginx 统计日志:

grep "xx/Sep/2022:1[8|9]" access.202209xx.log | awk '{x[$8]++;} END{for(i in x) print(i ":" x[i])}'

发现发生故障的时间段(晚上18xx~19:xx)内http 404错误特别多,这是一个异常的情况。

ec9328b28ac44365abe89e1116f28e9b.png

 2、初步判断http 404请求导致cookie失效。

当前时间段的nginx的404日志突增这么多,这是一个诡异的初步判断可能是404请求引起cookie失效的问题。

3、验证问题:

我们通过反复请求404的url,确实存在服务无法登录的问题。

三、问题原因分析


1、了解springboot2.x处理http 404机制

springBoot 默认提供了一个全局的 handler 来处理所有的 HTTP 错误, 并把它映射为 /error。当发生一个 HTTP 错误:例如 404 错误时, SpringBoot 内部的机制会将页面转发向到 /error 中。

由于Spring MVC会根据不同的请求URL,匹配到不同的RequestMapping。当没有匹配到相应的RequestMapping,请求是不会经过controller处理。因此我们自己定义的全局异常处理GlobalExceptionHandler类中的@ControllerAdvice注解只处理经过Controller的异常,不经过Controller的异常不进行处理。

对于404的请求,在springboot1.x与springboot2.x中的处理方式不一样:
在springboot1.5.10中:当存在请求没有controller匹配请求后404,同时会直接转发到/error,这个时候我们可以直接判断request中的uri是否包含/error,如果有抛出异常,再@ControllerAdvice处理即可。

对于springboot2.0:当发生http 404时,不仅原始请求会来一次,同时会转发到/error再次请求。这时候如果有拦截器,则会拦截两次,比如请求/api/123,原始请求会拦截一次,发生404后重定向到/api/error,会再拦截一次。

我们在拦截器打印日志就能印证会看到两次日志:



newCodeMoreWhite.png

10a35e8337824865b55a4f9e9d4345a0.png

/error接口的默认是由BasicErrorController处理器处理:org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController

8b8b7895b51445fab7cff9405741f22f.png

 BasicErrorController是Spring默认配置的一个Controller,默认处理/error请求。BasicErrorController提供两种返回错误:

一种是页面返回,浏览器访问显示如下错误页面;

50cbf1cee8e64077a1acbbf5e406f657.png

另外一种是json请求的时候就会返回json错误:

{
    "timestamp": "2022-10-06T14:46:33.686+00:00",
    "status": 404,
    "error": "Not Found",
    "path": "/213df/sfasd"
}

2、我们项目cookie失效的原因

我们token的处理机制是:拦截器、threadlocal、过滤器+AutoCloseable接口

1、使用拦截器拦截用户请求:使用拦截器拦截用户请求,当拦截器判断controller实例的方法包含TokenRequire注解,表示需要登录token才能访问。

2、threadlocal缓存:通过getTokenBySession()获取用户cookie信息,然后缓存到ContextLocal类的threadlocal变量。确保上下文可以获取到用户登录信息。

3、过滤器+AutoCloseable接口实现请求结束后清除ThreadLocal变量内容:ContextLocal通过实现AutoCloseable接口的close方法,在继承OncePerRequestFilter的过滤器里面通过try (resource)  {...}结构保证能释放ThreadLocal关联的实例。

线程ThreadLocal详解_hguisu的博客-CSDN博客

servlet的过滤器Filter详解_hguisu的博客-CSDN博客

 token类:

 拦截器:



newCodeMoreWhite.png

ContextLocal的ThreadLocal 变量缓存token:



newCodeMoreWhite.png

获取token逻辑:

1、发生http 404错误的时候:由于handler的对应类型不是Controller实例,即handler instanceof HandlerMethod为false。不会进入拦截器的业务逻辑模块。

2、然后spring boot内部转发向到/error接口,请求再次被拦截器拦截,但是过滤器不会再处理:

     1)转发向到/error接口,再次进入拦截器:由于接口/error的处理器是BasicErrorController,该BasicErrorController是Controller实例,即handler instanceof HandlerMethod为true。

     2)然后进入拦截器的业务逻辑模块。这里打印日志:

logger.info("api-log:[{}][{}][{}][{}]",getIp(request), tokenService.getLogAccountCode(), getApiUrl(method));

        打印日志地方调用tokenService.getLogAccountCode()获取用户账号,getLogAccountCode()方法又调用getTokenBySession()方法。

         错误的原因就是在这:

         进入getTokenBySession后,galaxyTokenLocal取值null,重新设置变量threadLocal的CURRENT_TOKEN为 GlobalToken.EMPTY。

if (galaxyTokenLocal == null) {
            GlobalToken globalToken = getTokenBySessionWithoutCache();
            //获得null时,用empty占位,表示已初始化
            ContextLocal.setCurrentToken(galaxyToken == null ? GlobalToken.EMPTY : galaxyToken);
            return globalToken;
        }

3、过滤器不会再处理,导致无法清除CURRENT_TOKEN=GlobalToken.EMPTY的情况:

/error接口是服务内forward转发的请求,继承OncePerRequestFilter的过滤器不会做对请求做处理。因此ContextLocal类不会执行close方法,即不会清除掉CURRENT_TOKEN=GlobalToken.EMPTY的情况。

4、当正确url请求的时候,由于上个threadLocal的CURRENT_TOKEN为GlobalToken.EMPTY没有被清理掉,此时GlobalToken globalTokenLocal = ContextLocal.getCurrentToken()为GlobalToken.EMPTY,最终getTokenBySession()返回null,导致无法获取用户token,一直提示用户token失效。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK