7.3 拦截器

7.3 拦截器

Interceptor拦截器是SpringMVC中相当重要的功能,它的主要作用是拦截用户的请求并进行相应的处理。比如**通过拦截器来进行用户权限验证,或者用来判断用户是否已经登录**等。

Spring MVC拦截器可插拔

Spring MVC拦截器是可插拔式的设计。如果需要使用某个拦截器,只需要在配置文件中应用该拦截器即可;如果不需要使用该拦截器,只需要在配置文件中取消应用该拦截器。不管是否应用某个拦截器,对Spring MVC框架不会有任何影响。

7.3.1 HandlerInterceptor接口

Spring MVC中的Interceptor拦截器拦截请求是通过实现HandlerInterceptor接口来完成的。在Spring MVC中定义一个Interceptor拦截器非常简单,通常在要定义的Interceptor拦截器类中实现SpringHandlerInterceptor接口,或者继承抽象类HandlerInterceptorAdapter
HandlerInterceptor接口中定义了三个方法,SpringMVC就是通过这三个方法来对用户的请求进行拦截处理的。

preHandle方法

preHandle方法的方法声明如下

1
boolean preHandle(HttpServletRequest request,HttpServletResponse response,Object handle)

顾名思义,该方法将在请求处理之前被调用
SpringMVC中的Interceptor实行的是链式调用,即在一个应用中或者说在一个请求中可以同时存在多个Interceptor。每个Interceptor的调用会依据它的声明顺序依次执行,而且最先执行的是Interceptor中的preHandle方法,所以可以在这个方法中进行一些前置的初始化操作或者是对当前请求的一个预处理,也可以在这个方法中进行一些判断来决定请求是否要继续进行下去。该方法的返回值是boolean类型的,

  • 当返回值为false时,表示请求结束,后续的InterceptorController都不会再执行;
  • 当返回值为true时就会继续调用下一个InterceptorpreHandle方法;

如果已经是最后一个Interceptor,就会调用当前请求的Controller方法。

postHandle方法

1
2
void postHandle (HttpServletRequest request,HttpServletResponse response,
Object handler,ModelAndView mv)

该方法和之后的afterCompletion方法都只能在当前所属的InterceptorpreHandle方法的返回值为true时才能被调用。
postHandle方法,顾名思义,就是在当前请求被处理之后,也就是在Controller的请求处理方法被调用之后执行,但是它会DispatcherServlet进行视图返回渲染之前被调用,所以我们可以在这个方法中对Controller处理之后的ModelAndView对象进行操作。postHandle方法被调用的方向跟preHandle是相反的,也就是说先声明的InterceptorpostHandle方法反而会后执行,这和Struts2里面的Interceptor的执行过程类似。

afterCompletion方法

1
2
void afterCompletion(HttpServletRequest request,HttpServletResponsere sponse,
Object handler,Exception exception)

该方法也是在当前所属的InterceptorpreHandle方法的返回值为true时才会执行。

顾名思义,该方法将在整个请求结束之后,也就是在DispatcherServlet渲染了对应的视图之后执行。这个方法的主要作用是进行资源清理

示例 拦截器实现用户权限验证

目录结构

展开/折叠
G:\Desktop\随书源码\Spring+Mybatis企业应用实战(第2版)\codes\07\InterceptorTest
├─src\
│ └─org\
│   └─fkit\
│     ├─controller\
│     │ ├─BookController.java
│     │ ├─FormController.java
│     │ └─UserController.java
│     ├─domain\
│     │ ├─Book.java
│     │ └─User.java
│     └─interceptor\
│       └─AuthorizationInterceptor.java
└─WebContent\
  ├─images\
  │ ├─ajax.jpg
  │ ├─android.jpg
  │ ├─basic.jpg
  │ ├─ee.jpg
  │ ├─fkjava.jpg
  │ ├─framework.jpg
  │ ├─java.jpg
  │ ├─javaee.jpg
  │ ├─struts.jpg
  │ └─xml.jpg
  ├─META-INF\
  │ └─MANIFEST.MF
  └─WEB-INF\
    ├─content\
    │ ├─loginForm.jsp
    │ └─main.jsp
    ├─lib\
    │ ├─commons-logging-1.2.jar
    │ ├─javax.servlet.jsp.jstl-1.2.1.jar
    │ ├─javax.servlet.jsp.jstl-api-1.2.1.jar
    │ ├─spring-aop-5.0.1.RELEASE.jar
    │ ├─spring-aspects-5.0.1.RELEASE.jar
    │ ├─spring-beans-5.0.1.RELEASE.jar
    │ ├─spring-context-5.0.1.RELEASE.jar
    │ ├─spring-context-indexer-5.0.1.RELEASE.jar
    │ ├─spring-context-support-5.0.1.RELEASE.jar
    │ ├─spring-core-5.0.1.RELEASE.jar
    │ ├─spring-expression-5.0.1.RELEASE.jar
    │ ├─spring-instrument-5.0.1.RELEASE.jar
    │ ├─spring-jcl-5.0.1.RELEASE.jar
    │ ├─spring-jdbc-5.0.1.RELEASE.jar
    │ ├─spring-jms-5.0.1.RELEASE.jar
    │ ├─spring-messaging-5.0.1.RELEASE.jar
    │ ├─spring-orm-5.0.1.RELEASE.jar
    │ ├─spring-oxm-5.0.1.RELEASE.jar
    │ ├─spring-test-5.0.1.RELEASE.jar
    │ ├─spring-tx-5.0.1.RELEASE.jar
    │ ├─spring-web-5.0.1.RELEASE.jar
    │ ├─spring-webflux-5.0.1.RELEASE.jar
    │ ├─spring-webmvc-5.0.1.RELEASE.jar
    │ └─spring-websocket-5.0.1.RELEASE.jar
    ├─springmvc-config.xml
    └─web.xml

本小节通过拦截器完成一个用户权限验证的功能。即用户必须登录之后才可以访问网站首页,如果没有登录就直接访问网站首页,则拦截器会拦截请求,并将请求重新转发到登录页面,同时提示用户需要先登录再访问网站。

FormController.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 动态页面跳转控制器
*/
@Controller
public class FormController {

@GetMapping(value = "/loginForm")
public String loginForm()
{
System.out.println("请求处理方法 loginForm");
// 跳转到登录页面
return "loginForm";
}
}

loginForm.jsp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>登录页面</title>
</head>
<body>
<h3>登录页面</h3>
<form action="login" method="post">
<!-- 提示信息 -->
<font color="red">${requestScope.message }</font>
<table>
<tr>
<td><label>登录名: </label></td>
<td><input type="text" id="loginname" name="loginname"></td>
</tr>
<tr>
<td><label>密码: </label></td>
<td><input type="password" id="password" name="password"></td>
</tr>
<tr>
<td><input type="submit" value="登录"></td>
</tr>
</table>
</form>
</body>
</html>

UserController.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package org.fkit.controller;

import javax.servlet.http.HttpSession;
import org.fkit.domain.User;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.servlet.ModelAndView;

/**
* 处理用户请求控制器
*/
@Controller
public class UserController
{

/**
* 处理/login请求
*/
@PostMapping(value = "/login")
public ModelAndView login(String loginname, String password,
ModelAndView mv, HttpSession session)
{
// 模拟数据库根据登录名和密码查找用户,判断用户登录
if (loginname != null && loginname.equals("fkit") && password != null
&& password.equals("123456"))
{
// 模拟创建用户
User user = new User();
user.setLoginname(loginname);
user.setPassword(password);
user.setUsername("管理员");
// 登录成功,将user对象设置到HttpSession作用范围域
session.setAttribute("user", user);
// 转发到main请求
mv.setViewName("redirect:main");
} else
{
// 登录失败,设置失败提示信息,并跳转到登录页面
mv.addObject("message", "登录名或密码错误,请重新输入!");
mv.setViewName("loginForm");
}
return mv;
}

}

UserController类的login方法用来处理登录请求,本示例没有使用数据库存储数据,只是简单地模拟了用户登录,只要用户输入的登录名是”xiaoming“,密码是”xiaoming”,则验证通过。

  • 如果验证通过,创建一个User对象保存到HttpSession当中,同时将请求使用客户端跳转到main请求;
  • 如果验证失败,则设置失败提示信息到ModelAndView对象,同时将请求使用客户端跳转到loginForm请求,即跳回登录页面。

BookController.java

BookController类的main方法用来处理网站首页的请求,该方法获得所有图书信息,并将它们设置到Model当中,然后传递到main页面。本示例没有使用数据库存储数据,只是简单地创建了一个集合模拟从数据库获取图书信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package org.fkit.controller;

import java.util.ArrayList;
import java.util.List;
import org.fkit.domain.Book;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

/**
* 处理图书请求控制器
*/
@Controller
public class BookController
{

/**
* 处理/main请求
*/
@RequestMapping(value = "/main")
public String main(Model model)
{
// 模拟数据库获得所有图书集合
List<Book> book_list = new ArrayList<Book>();
book_list.add(new Book("java.jpg", "疯狂Java讲义(附光盘)", "李刚 编著", 74.2));
book_list.add(new Book("ee.jpg", "轻量级Java EE企业应用实战", "李刚 编著", 59.2));
book_list.add(
new Book("android.jpg", "疯狂Android讲义(附光盘)", "李刚 编著", 60.6));
book_list.add(new Book("ajax.jpg", "疯狂Ajax讲义(附光盘)", "李刚 编著", 66.6));
// 将图书集合添加到model当中
model.addAttribute("book_list", book_list);
// 跳转到main页面
return "main";
}

}

main.jsp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>首页</title>
<style type="text/css">
table {
border-collapse: collapse;
border-spacing: 0;
border-left: 1px solid #888;
border-top: 1px solid #888;
background: #efefef;
}

th, td {
border-right: 1px solid #888;
border-bottom: 1px solid #888;
padding: 5px 15px;
}

th {
font-weight: bold;
background: #ccc;
}
</style>
</head>
<body>
<h3>欢迎[${sessionScope.user.username }]访问</h3>
<br>
<table border="1">
<tr>
<th>封面</th>
<th>书名</th>
<th>作者</th>
<th>价格</th>
</tr>
<c:forEach items="${requestScope.book_list }" var="book">
<tr>
<td><img src="images/${book.image }" height="60"></td>
<td>${book.name }</td>
<td>${book.author }</td>
<td>${book.price }</td>
</tr>
</c:forEach>
</table>
</body>
</html>

拦截器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
/**
* 拦截器必须实现HandlerInterceptor接口
*/
public class AuthorizationInterceptor
implements HandlerInterceptor
{
// 不拦截"/loginForm"和"/login"请求
private static final String[] IGNORE_URI = {"/loginForm", "/login"};
/**
* 该方法将在整个请求完成之后执行, 主要作用是用于清理资源的,
* 该方法也只能在当前Interceptor的preHandle方法的返回值为true时才会执行。
*/
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response, Object handler,
Exception exception)
throws Exception
{
System.out.println("拦截器的 afterCompletion方法\n-----------------------------");
}
/**
* 该方法将在Controller的方法调用之后执行, 方法中可以对ModelAndView进行操作 ,
* 该方法也只能在当前Interceptor的preHandle方法的返回值为true时才会执行。
*/
@Override
public void postHandle(HttpServletRequest request,
HttpServletResponse response, Object handler,
ModelAndView mv)
throws Exception
{
System.out.println("拦截器的 postHandle方法");
}
/**
* preHandle方法是进行处理器拦截用的,该方法将在Controller处理之前进行调用,
* 该方法的返回值为true拦截器才会继续往下执行,该方法的返回值为false的时候整个请求就结束了。
*/
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler)
throws Exception
{
System.out.println("拦截器的 preHandle方法");
// flag变量用于判断用户是否登录,默认为false
boolean flag = false;
// 获取请求的路径进行判断
String servletPath = request.getServletPath();
// 判断请求是否需要拦截
for (String s : IGNORE_URI)
{
if (servletPath.contains(s))
{
flag = true;
break;
}
}
// 拦截请求
if (!flag)
{
// 1.获取session中的用户
User user = (User) request.getSession().getAttribute("user");
// 2.判断用户是否已经登录
if (user == null)
{
// 如果用户没有登录,则设置提示信息,跳转到登录页面
System.out.println(" |----验证不通过,重新登录");
request.setAttribute("message", "请先登录再访问网站");
// 转发到登录页面
request.getRequestDispatcher("loginForm").forward(request,
response);
} else
{
// 如果用户已经登录,则验证通过,放行
System.out.println(" |----验证通过,放行/main请求");
flag = true;
}
}
return flag;
}
}

测试

1
<a href="main">直接请求/main</a>

如果没有登录,直接访问main请求,拦截器会拦截请求,验证用户是否登录,此时用户若没有登录,则跳转到登录页面,如下图所示:
这里有一张图片
从控制台的输出可以看到各个方法的执行顺序,如下所示.

1
2
3
4
5
6
7
拦截器的 preHandle方法
|----验证不通过,重新登录
拦截器的 preHandle方法
请求处理方法 loginForm
拦截器的 postHandle方法
拦截器的 afterCompletion方法
-----------------------------

此时填写正确的用户名xiaoming和密码xiaoming,点击登录
这里有一张图片
此时才可以正确得到main请求的资源,如下图所示:
这里有一张图片
控制台新增内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
拦截器的 preHandle方法
请求处理方法 login
|----->登陆验证通过,转发到main请求
拦截器的 postHandle方法
拦截器的 afterCompletion方法
-----------------------------
拦截器的 preHandle方法
|----验证通过,放行/main请求
请求处理方法 main
拦截器的 postHandle方法
拦截器的 afterCompletion方法
-----------------------------

可以看到执行顺序是,表单提交到login请求,login方法验证成功后,转发到main请求,当请求main时,拦截器的preHandle方法先验证是否登录过,现在的情况是已经登录了,preHandle方法放行main请求,main方法得到执行.