Spring MVC中的Spring HandlerInterceptor -Yuri Mednikov


这篇文章描述了Spring的HandlerInterceptor的定义,它与Java Servlet的HttpFilters的不同之处,以及HandlerInterceptor接口方法的概述以及如何在应用程序中配置它们。
在软件开发中,有很多情况下,您需要以某种方式捕获到服务器的请求并进行一些预处理或后处理。例如,当您需要验证是否在执行此路径之前授权用户访问特定路径时,必须进行身份验证。Spring作为Spring Web的一部分提供了一种使用SpringInterceptor对象的方法。它们允许在控制器处理处理程序之前或之后提供自定义逻辑。
 
什么是Spring HanlderInterceptor?
HandlerInterceptor是一个Spring Web接口,它允许实现组件以拦截客户端请求并提供自定义处理逻辑。这样的处理可以在Spring控制器处理请求之前或之后执行。处理程序拦截器实现org.springframework.web.servlet.HandlerInterceptor了提供三种核心方法的接口:

  • boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object handler)
  • void postHandle(HttpServletRequest req, HttpServletResponse res, Object handler, ModelAndView mw)
  • void afterCompletion(HttpServletRequest req, HttpServletResponse res, Object handler, Exception ex)

开发人员可以注册任意数量的拦截器,以及将它们附加到特定的处理程序组并控制其顺序。此类功能将HandlerInterceptor与另一个工具HttpFilters关联。
 
创建HandlerInterceptor
如前所述,为了创建拦截器,开发人员必须实现HanlderInterceptor接口。由于其所有方法均为默认方法,因此您可以为所有方法或仅为所需方法提供实施。HanlderInterceptor通常用于实现基于令牌的身份验证。让我们看一下使用的基本情况HandlerInterceptor:

@Component
public class TokenInterceptor implements HandlerInterceptor {

    @Autowired private AuthService service;

    @Override
    public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object handler) throws Exception {
        String authToken = req.getHeader("Authorization");
        if (authToken==null) throw new RuntimeException();
        return service.validateToken(authToken);
    }
}

在此代码段中,我们创建了一个自定义拦截器组件,以检查传入的请求是否确实附加了有效的身份验证令牌。请注意,与HttpFilters不同,HandlerInterceptor提供了对HttpServletRequest和HttpServletResponse组件的访问,因此您可以直接使用它们而无需从ServletRequestand 进行强制转换ServletResponse。
我们还将TokenInterceptor类注解为Spring的@Component,以使框架知道在基于注解的配置中以这种方式对其进行处理。
 
接口方式
接口提供了三种默认方法。本节概述了它们。
1. preHandle()
此方法拦截请求处理的执行。在控制器处理请求并返回布尔值(用于确定下一步处理)之前将其触发。如果方法返回truepreHandle(),则请求转到处理程序。如果返回错误,则请求被中断。此方法接受三个参数:

  • HttpServletRequest 是当前的HTTP请求
  • HttpServletResponse 是当前的HTTP响应
  • Handler 是选择的处理程序对象来处理请求

如前所述,该方法可用于验证先决条件,例如令牌。我们还可以使用将信息传递给处理程序HttpServletRequest。看一下下面的代码片段:
@Override
public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object handler) throws Exception {
    String token = req.getHeader("Authorization");
    Optional<User> user = service.parseToken(token);
    if (user.isPresent()){
        req.setAttribute(
"user", user.get());
        return true;
    } else {
        return false;
    }
}

2.postHandle()
在 Spring控制器处理程序之后,但在呈现视图之前(如果您构建的是遵循MVC架构的应用程序),将触发此方法。因此,在这种情况下,您可以修改ModelAndView将要显示的内容并附加其他数据。此方法接受四个参数:

  • HttpServletRequest 是当前的HTTP请求
  • HttpServletResponse 是当前的HTTP响应
  • Handler 是选择的处理程序对象来处理请求
  • ModelAndView 是处理程序ModelAndView对象返回的。

作为使用这种方法的示例,我们可以提供一种情况,当我们想向用户显示用于渲染页面的时间时。为此,我们还需要首先使用捕获初始时间preHandle。看一下以下代码片段:
@Override
public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object handler) throws Exception {
    long started = System.currentTimeMillis();
    req.setAttribute("startedTime", started);
    return true;
}

@Override
public void postHandle postHandle(HttpServletRequest req, HttpServletResponse res, Object handler, ModelAndView modelAndView) throws Exception {
    long started = Long.valueOf(req.getAttribute(
"startedTime").toString());
    long finished = System.currentTimeMillis();
    long rendered = finished - started;
    modelAndView.addObject(
"renderedTime", rendered);
}

3.afterCompletion()
最后,这里的第三个方法afterCompletion()是在处理程序处理完并向用户呈现视图之后调用。请注意,在处理程序执行的任何情况下都将调用此方法,但前提是相应的preHandle()方法返回true。它接受四个参数:

  • HttpServletRequest 是当前的HTTP请求
  • HttpServletResponse 是当前的HTTP响应
  • Handler 是选择的处理程序对象来处理请求
  • Exception是处理程序抛出的异常,除非,它是通过异常解析器处理的

我们可以使用这种方法来记录处理程序处理的结果,如以下代码所示:
// assume we have a logger
private static final Logger logger = LoggerFactory.getLogger(SampleInterceptor.class);

@Override
public void afterCompletion(HttpServletRequest req, HttpServletResponse res, Object handler, Exception ex)
    throws Exception{
        String destination = req.getRequestUrl();
        int result = res.getStatus();
        logger.info(
"The request to the destination {} was processed with the result {}", destination, result);
}

 
配置
为了使用处理程序拦截器,我们需要使用自定义WebMvcConfigurer配置实例对其进行配置。下面的代码演示了令牌拦截器配置的基本用法:

@Configuration
public class TokenInterceptorConfig implements WebMvcConfigurer {

    @Autowired
    private TokenInterceptor tokenInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(tokenInterceptor);
    }

}

此代码将拦截器附加到所有处理程序。我们还用Spring注释了它,@Configuration以告诉框架它用于定义bean,并且Spring容器将在bean生成中使用。虽然,这是基本情况。我们还可以:
  • 将拦截器附加到特定的路由/路由组
  • 从特定路由/路由组中排除拦截器
  • 定义拦截器执行的顺序

为此,我们需要处理InterceptorRegistration作为返回的对象addInterceptor()。
 

包含路径
要为特定路径或路由组附加拦截器,我们可以利用addPathPatterns()允许指定某些路径的方法。它有两个重载版本:

  • addPathPatterns(List<String> patterns)以数组列表的形式接受一组模式
  • addPathPatterns(String... patterns) 接受varagrs形式的模式

由于我们有令牌拦截器,因此我们希望将其应用于所有以开头的路由/secured/:

@Override 
public void addInterceptors(InterceptorRegistry registry){
    registry.addInterceptor(tokenInterceptor).addPathPatterns("/secured/**");
}

 
排除路径
同样,我们可以从拦截器中排除一些路径。可以使用excludePathPatterns()method 来完成,它可以有两种形式:
  • excludePathPatterns(List<String> patterns) 接受列表形式的模式
  • excludePathPatterns(String... patterns) 接受模式作为varargs

比如我们要排除的路径/login,/signup并/restore-password从令牌验证(因为它们还没有任何安全):
@Override 
public void addInterceptors(InterceptorRegistry registry){
    List<String> excludedRoutes = Arrays.asList("/login", "/signup", "/restore-password");
    registry.addInterceptor(tokenInterceptor).addPathPatterns(
"/secured/**").excludePathPatterns(excludedRoutes);
}

 
定义顺序
最后,如果我们使用多个HandlerInterceptor,我们可以指定它们的执行顺序。这是通过order()从InterceptorRegistration接受一个int数量的拦截器顺序(从0开始)的方法调用的。例如,我们有三个拦截器:一个用于验证令牌,第二个用于验证用户的订阅状态,最后记录:

@Configuration
public class AppInterceptorConfig implements WebMvcConfigurer {

    //.. inject interceptors
    @Autowired private TokenInterceptor tokenInterceptor;
    @Autowired private SubscriptionInterceptor subscriptionInterceptor;
    @Autowired private LoggingInterceptor logInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(tokenInterceptor).order(0);
        registry.addInterceptor(subscriptionInterceptor).order(1);
        registry.addInterceptor(logInterceptor).order(2);
    }

}