伪原创网站源码(【干货】临时接手一个认证项目之不是报401错误)

优采云 发布时间: 2022-02-27 14:02

  伪原创网站源码(【干货】临时接手一个认证项目之不是报401错误)

  前言

  我最近加入了一家新公司,暂时接手了一个认证项目。对于像我这种对优雅代码有强迫症的人来说,毫无疑问,我应该改变我看到它时不舒服的代码!改变!改变!但是改了之后,前端给我反馈说界面老是报401错误。我的心:我的草?我修复了错误吗?不应该的,这么简单的东西怎么会有bug!于是我自己测试了一下,确实有问题,不过不是我的问题,下面开始分析吧!

  伪代码场景还原

  登录界面,模拟报错

  @PostMapping("/user/login")

public LoginResult login(@RequestBody LoginRequest request) {

throw new RuntimeException("模拟登录接口报错");

}

  然后post一个*敏*感*词*,如果认证请求没有携带token,或者redis中找不到token相关的用户,就会抛出异常

  public class UserLoginInterceptor implements HandlerInterceptor {

@Override

public boolean preHandle( HttpServletRequest request, HttpServletResponse response, Object handler) {

String token = request.getHeader("token");

if (token == null) {

throw new UnauthorizedException("未认证或token已过期");

} else {

if(redis.get(token) == null) {

throw new UnauthorizedException("未认证或token已过期");

}

//...将token和用户信息设置到 ThreadLocal

}

return true;

}

}

  *敏*感*词*配置

  @Configuration

public class WebConfig implements WebMvcConfigurer {

@Override

public void addInterceptors(InterceptorRegistry registry) {

registry.addInterceptor(new UserLoginInterceptor())

.excludePathPatterns("/user/login")

.addPathPatterns("/**");

}

}

  在本项目中,从*敏*感*词*中获取token去Redis查看用户信息,放到ThreadLocal中。由于 Controller → Service → Mapper 的请求具有相同的线程 ID,这样的请求链可以从这个 ThreadLocal 中获取当前登录。用户信息。可以看到/user/login被*敏*感*词*释放了。但是,当这个请求的控制器报错时,预期的消息信息应该是模拟登录界面报的错误,但是在运行时报如下未验证的错误,说明我们的请求到了*敏*感*词*

  {

"path": "/error",

"message": "com.yinshan.auth.core.exception.UnauthorizedException: 未认证或token已过期",

"error": "Unauthorized",

"status": 401,

"timestamp": "2021-09-22T14:03:39.986559500"

}

  当然,这个错误信息的格式是我自己处理的。这并不重要。重点是我在登录界面报了500错误。为什么它在*敏*感*词*中变成了 401 未经身份验证。

  调试分析

  废话不多说,直接开始调试,在抛出异常的代码上下断点,然后在*敏*感*词*中下断点

  

  

  结果在登录界面按F9后,断点确实到了*敏*感*词*,

  

  说实话,我当时真的有这个表情。为什么已经被*敏*感*词*释放的接口向*敏*感*词*报错?但是,仔细查看调试面板中 preHandle 方法的请求参数细节,发现了一些棘手的问题。

  

  图中的箭头指向重要信息:

  看到这里,一般都清楚这个断点到了*敏*感*词*,不是因为/user/login请求,而是另一个/error请求。那么这个 /error 是怎么来的呢?由于图中的TomcatEmbededContext上下文是Tomcat中嵌入SpringBoot的一个类,我猜这个请求应该是SpringMVC控制器遇到未处理错误时内部重新发起的/error请求。

  也许你会想,这不是问题吗?好像很快,怎么搞了两个小时?因为我的食物!调试的时候,根本不在意这个参数是什么,而是一步步F8→F7→F8→F7……。. 终于在调试DispatchServlet的时候才意识到,请求是怎么转发的,终于明白了,大家都麻木了。

  查询官方文档

  果然在SpringMVC的官方文档中找到了说明

  

  官网明确表示,如果异常没有被默认的异常处理器处理,servlet容器会使用DispatchServlet来分派/error请求,也可以自定义/error请求。详细请参考SpringMVC官方文档

  具体原因

  SpringMVC控制器报错后,服务器会发出/error请求。由于我们的*敏*感*词*没有释放/error请求,所以该请求的*敏*感*词*会在DispatchServlet中执行(突然想起两年前我写了一个自定义的SpringBoot异常页面,就是处理后的/error请求)

  protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {

//...

//判断并执行*敏*感*词*的 preHandle()

if (!mappedHandler.applyPreHandle(processedRequest, response)) {

return;

}

  以上是 DispatchServlet 的 doDispatch 部分的源码。相信大部分人对doDispatch的理解停留在采访背诵SpringMVC执行流程的时候。其实网上为SpringMVC的执行过程画的图都是关键节点,没有那么详细。如果你没有真正调试过这个有问题的源代码,那么你可能不理解这个问题。

  解决方案

  一旦你了解了问题的原因,解决方案就很简单了。只需在我们自定义的认证*敏*感*词*中排除/error的拦截即可

  @Override

public void addInterceptors(InterceptorRegistry registry) {

registry.addInterceptor(new UserLoginInterceptor())

.excludePathPatterns("/error").addPathPatterns("/**");

}

  谈论*敏*感*词*

  其实上面的问题很大一部分是因为对*敏*感*词*没有真正的了解,只是知道它可以拦截一个请求,却没有研究在什么阶段拦截,以及在SpringMVC中如何实现。那么让我们仔细看看*敏*感*词*

  *敏*感*词*和过滤器的使用范围

  查看Filter接口的源码可以发现它在javax.servlet包下,HandlerInterceptor在org.springframework.web.servlet包下。*敏*感*词*由 SpringMVC 实现。实际上,它只是一个或多个 Java 类的组合。只是拦截,与web应用没有必然联系。这意味着过滤器只能在 Web 应用程序中使用,而*敏*感*词*可以在任何可以使用 Spring 和 SpringMVC 的地方使用,例如桌面应用程序。

  *敏*感*词*和过滤器的执行顺序和执行流程

  过滤器的执行是在请求到达 servlet 之前通过 ApplicationFilterChain.doFilter() 进行链式调用。在 doFilter() 内部获取下一个过滤器实例,并执行过滤器方法。其执行顺序为 filter1 → ApplicaitonFilterChain.doFilter( ) → filter2 → ApplicationFilterChain.doFilter() → filter3 →  

  如下所示

  

  *敏*感*词*的执行是在请求到达DispatchServlet后,在Controller方法执行前后做的事情,如下图所示。这里的过滤器链就是上图。

  

  显然preHandle()是拦截的关键,只是在请求到达Controller目标方法之前执行,它通过返回true/false来判断请求是否需要拦截。

  doDispatch内部*敏*感*词*处理部分的源码

  我们都知道 DispatchServlet 的 doDispatch() 方法处理所有请求。*敏*感*词*相关的内部代码如下

  //调用 Controller 目标方法前执行*敏*感*词*的 preHandle()

if (!mappedHandler.applyPreHandle(processedRequest, response)) {

return;

}

mv = ha.handle(processedRequest, response, mappedHandler.getHandler());//反射调用 Controller 目标方法

/**

* ...省略

* */

mappedHandler.applyPostHandle(processedRequest, response, mv);//Controller 目标方法执行完后调用*敏*感*词* postHandle()

//请求完成之后执行*敏*感*词*的 afterCompletion()

processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);

  其实如果真的调试出有问题的源码,根本不需要背SpringMVC执行过程面试题~~~我也记不住,但是从源码调试过程中,我已经知道了DispatchServlet 正在进行请求转发。做完这些,结合前面提到的参数校验神器hibernate-validator和统一的异常处理,自然就明白SpringMVC是如何实现请求参数的解析和转换了。

  结语

  遇到问题不要慌张,源码调试没那么难,我觉得有问题的看源码比较有印象。来到新公司不到一个月,带着疑问读了好几遍源代码……正好赶上大版的技术组件,总有各种奇怪的问题。

  多看一下框架和技术组件的官方文档确实是一个非常好的习惯,并不总是局限于某些视频教程。阅读官方文档,找出组件可能出现的问题以及问题的原因。

0 个评论

要回复文章请先登录注册


官方客服QQ群

微信人工客服

QQ人工客服


线