前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >架构师技能8:springboot全局handler处理http 404错误引发登录失效的问题

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

作者头像
黄规速
发布2022-12-03 12:45:53
1.1K0
发布2022-12-03 12:45:53
举报

开篇语录:以架构师的能力标准去分析每个问题,过后由表及里分析问题的本质,复盘总结经验,并把总结内容记录下来。当你解决各种各样的问题,也就积累了丰富的解决问题的经验,解决问题的能力也将自然得到极大的提升。 励志做架构师的撸码人,认知很重要,可以订阅:架构设计专栏

一、背景


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

二、问题定位


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

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

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

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

发现发生故障的时间段(晚上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,会再拦截一次。

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

代码语言:javascript
复制
@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关联的实例。

代码语言:javascript
复制
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);
        }
    }
}

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

 token类:

代码语言:javascript
复制
@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分钟过期
}

 拦截器:

代码语言:javascript
复制
    @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:

代码语言:javascript
复制
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逻辑:

代码语言:javascript
复制
    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失效。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2022-12-01,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、背景
  • 二、问题定位
    • 1、通过nginx日志检查http的请求异常信息:
        •  2、初步判断http 404请求导致cookie失效。
          • 3、验证问题:
          • 三、问题原因分析
            • 1、了解springboot2.x处理http 404机制
              • 2、我们项目cookie失效的原因
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档