之前用 Django 开发的时候,Django 内置的 middleware 提供了 login_required() 装饰器作登录拦截。强大的 Spring MVC 也支持拦截器,可以通过不算复杂的配置非常灵活的控制请求拦截策略。拦截器普遍用在用户登录验证上,也应用在其他需要对一些信息进行验证的场景下。

实现拦截

请求流程

Spring MVC 请求的生命周期

图示给出了一次请求从发送到处理到接收响应的整个过程,非常标准的 M-V-C。

接口实现

Spring MVC 拦截器由 HandlerInterceptor 实现。HandlerInterceptor 接口包含三个方法:

public interface HandlerInterceptor {
    boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws Exception;

    void postHandle(HttpServletRequest req, HttpServletResponse resp, Object handler, ModelAndView modelAndView) throws Exception;

    void afterCompletion(HttpServletRequest req, HttpServletResponse resp, Object handler, Exception ex) throws Exception;
}

从这三个方法名就能看出各自执行的事件节点:分别在请求处理之前、请求处理之后但在渲染视图之前、请求完成之后。

preHandle() 在请求进到 Controller 前就对请求进行预处理。如果处理结果返回 true 则请求放行并继续往下执行,进到 Controller 或 下一个拦截器中;如果处理结果为 false 则中断处理请求,直接返回响应。

postHandle() 只有当 preHandle() 返回 true 时才会执行,也就是在请求进入到 Controller 之后再执行。它可以对 ModelAndView 进行处理,再返回给前端进行渲染。

afterCompletion() 在请求被完整处理完成后执行,也就是在渲染视图后。

拦截器的关键就是在请求处理之前将其拦截,所以最重要的方法就是 preHandle(),它是必须要实现的,而 postHandle()afterCompletion() 的实现可以为空。Spring MVC servlet.handler 包里内置的 HandlerInterceptorAdapter 适配器实现 HandlerInterceptor 接口的 preHandle() 方法。通常可以直接继承该适配器。

public class ExampleInterceptor extends HandlerInterceptorAdapter {
    @Override
    public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object obj) throws Exception {
        // hanle the request...
        if (OK) {
            System.out.println(req.GetRuestURI());
            return true;
        } else {
            resp.sendRedirect("http://isudox.com");
            return false;
        }
    }
}

拦截配置

Spring MVC 配置文件通过 mvc:interceptors 标签声明并配置拦截器链,拦截的顺序由声明的顺序确定。其中,mvc:mapping 标签指定要拦截的 URL 以及忽略的 URL,支持通配符;bean 标签指定处理该 URL 的拦截器。

<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mvc="http://www.springframework.org/schema/mvc"
	xmlns:context="http://www.springframework.org/schema/context"
	xsi:schemaLocation="
        http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd">

    <mvc:interceptors>
        <bean class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor" />
        <mvc:interceptor>
            <mapping path="/**"/>
            <exclude-mapping path="/admin/**"/>
            <bean class="org.springframework.web.servlet.theme.ThemeChangeInterceptor" />
        </mvc:interceptor>
        <mvc:interceptor>
            <mapping path="/secure/*"/>
            <bean class="org.example.SecurityInterceptor" />
        </mvc:interceptor>
    </mvc:interceptors>

</beans>

或者也可以通过 Java 代码来配置拦截器:

@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LocaleInterceptor());
        registry.addInterceptor(new ThemeInterceptor()).addPathPatterns("/**").excludePathPatterns("/admin/**");
        registry.addInterceptor(new SecurityInterceptor()).addPathPatterns("/secure/*");
    }

}

Ajax请求

对于拦截 Ajax 请求的场景,有些细节处理需要注意。拦截器通常需要获取请求来源页面的 URL,使其能在处理完成后能返回之前的页面,比如从某个页面跳转到登录页,登录后再跳转回之前浏览的页面。但如果是 Ajax 请求,比如在页面上点击一个按钮发起请求,改变局部页面元素或行为,这时候拦截器所需要的来源页 URL 被记录在请求头的 referer 中。

首先判断一个请求是否为 Ajax。Ajax 请求的头部会带上 X-Requested-With:XMLHttpRequest

private boolean isAsyncRequest(HttpServletResponse request) {
    String header = request.getHeader("X-Requested-With");
    return header != null && "XMLHttpRequest".equalsIgnoreCase(header);
}

获取 referer 信息

public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object obj) throws Exception {
    String referer = req.getHeader("Referer");
    JSONObject jsonObject = new JSONObject();
    jsonObject.put("requestURI", URLEncoder.encode(referer, "UTF-8"));
    resp.getWriter().write(JSONObject.toJSONString(jsonObject));
    return false;