架构师技能8:springboot全局handler处理http 404错误引发登录失效的问题
Posted hguisu
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了架构师技能8:springboot全局handler处理http 404错误引发登录失效的问题相关的知识,希望对你有一定的参考价值。
开篇语录:以架构师的能力标准去分析每个问题,过后由表及里分析问题的本质,复盘总结经验,并把总结内容记录下来。当你解决各种各样的问题,也就积累了丰富的解决问题的经验,解决问题的能力也将自然得到极大的提升。
一、背景
国庆前我们线上出现一次故障:用户无法登录某个微服务,后面一段时间后就自动恢复了,然后我持续跟踪和分析这个问题好久找到原因,顺便在此记录下来。
二、问题定位
出现这种问题,肯定是需要通过日志来定位,由于业务服务日志没有异常日常,因此通过外围日志来分析:
1、通过nginx日志检查http的请求异常信息:
在接入层的其中一台nginx 统计日志:
grep "xx/Sep/2022:1[8|9]" access.202209xx.log | awk 'x[$8]++; ENDfor(i in x) print(i ":" x[i])'
发现发生故障的时间段(晚上18xx~19:xx)内http 404错误特别多,这是一个异常的情况。
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,会再拦截一次。
我们在拦截器打印日志就能印证会看到两次日志:
@Component
public class ClusterParamInterceptor implements HandlerInterceptor
private static final Logger logger = LoggerFactory.getLogger(ClusterParamInterceptor.class);
@Override
public boolean preHandle(HttpServletRequest httpRequest,
HttpServletResponse response, Object handler) throws Exception
logger.info("httpRequest:", httpRequest.getRequestURL());
return true;
@Override
public void afterCompletion(HttpServletRequest httpRequest,
HttpServletResponse response, Object handler,
Exception ex) throws Exception
/error接口的默认是由BasicErrorController处理器处理:org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController
BasicErrorController是Spring默认配置的一个Controller,默认处理/error请求。BasicErrorController提供两种返回错误:
一种是页面返回,浏览器访问显示如下错误页面;
另外一种是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
关联的实例。
public class GlobalFilter extends OncePerRequestFilter
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException
try (ContextLocal ignored = new ContextLocal())
filterChain.doFilter(request, response);
token类:
@Data
public class GlobalToken implements Serializable
public static final GlobalToken EMPTY = new GlobalToken();
private static final long serialVersionUID = 1L;
private String id;
private Date createTime;
private Date lastAccessTime;/
private Date lastUpdateTime;
private Long timeout = 1000 * 60 * 30L;//默认30分钟过期
拦截器:
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception
if (handler != null && handler instanceof HandlerMethod)
HandlerMethod method = (HandlerMethod) handler;
logger.info("api-log:[][][][]",getIp(request), tokenService.getLogAccountCode(), getApiUrl(method));
TokenRequire tokenRequire = method.getMethod().getAnnotation(TokenRequire.class);
if (tokenRequire != null)
GlobalToken token = tokenService.getTokenBySession();
if (token != null)
//逻辑处理
else
//TODO 判断是否有cookie集成
throw new NullTokenException(method.getMethod().getName() + " get token is null");
return super.preHandle(request, response, handler);
ContextLocal的ThreadLocal 变量缓存token:
public class ContextLocal implements AutoCloseable
private static final ThreadLocal<GlobalToken> CURRENT_TOKEN = new ThreadLocal<>();
public static GlobalToken getCurrentToken()
return CURRENT_TOKEN.get();
public static void setCurrentToken(GlobalToken globalToken)
CURRENT_TOKEN.set(globalToken);
@Override
public void close()
MDC.remove(WebHeaderConstants.REQ_ID);
CURRENT_TOKEN.remove();
获取token逻辑:
public GlobalToken getTokenBySession()
GlobalToken globalTokenLocal = ContextLocal.getCurrentToken();
//未初始化
if (galaxyTokenLocal == null)
GlobalToken globalToken = getTokenBySessionWithoutCache();
//获得null时,用empty占位,表示已初始化
ContextLocal.setCurrentToken(galaxyToken == null ? GlobalToken.EMPTY : galaxyToken);
return globalToken;
return globalTokenLocal == GlobalToken.EMPTY ? null : globalTokenLocal;
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失效。
以上是关于架构师技能8:springboot全局handler处理http 404错误引发登录失效的问题的主要内容,如果未能解决你的问题,请参考以下文章
架构师技能8:springboot全局handler处理http 404错误引发登录失效的问题
作为一个刚刚入职Android开发的应届生,该如何走向架构师?