本章主要记录Spring MVC实现简单的权限控制拦截器和请求信息统计拦截器。本章涉及的知识点有:
- mvc:interceptors :Spring MVC拦截器的XML配置标签
- HandlerInterceptorAdapter:拦截器适配器,一般用来继承,以实现项目需要的拦截器。
- X-Requested-With:request
的header
的一种信息,用于标志此请求是否为ajax
请求。
- HttpServletRequestWrapper:request
请求的包装器,一般用来继承,用以实现对request
的包装器。
- OncePerRequestFilter:Spring封装的一个抽象过滤器,能够保证每个request
只过滤一次。
- ThreadLocal:线程内的局部变量。
1.关于拦截器的简介
拦截器是Spring AOP(Aspected-Oriented Programming,面向切面的编程)的一种实现,他的作用与Filter较为类似,就是实现了将一些公共代码抽离出来统一处理的功能。
拦截器的应用场景有很多,如:日志记录、权限检查、性能监控、通用行为等等。其实现原理都是一致的,就是在preHandle[方法执行前]
、postHandle[方法执行后,视图渲染前]
和afterCompletion[方法执行后]
这三个切面中,获取request
、response
、handler
等信息,然后对这些信息进行统一处理。
本章主要实现以下两种场景:
1. 获取每次业务请求的信息:控制器、方法、请求方式、请求路径、请求参数、方法执行耗时。
2. 简单的权限校验:检查每次请求中是否存在session字段,不存在跳转至登录页面。
2.简单权限校验拦截器
实现思路:通过重写preHandle
方法,在每次请求方法执行之前,对request
中的session
信息进行检查,然后做出相应处理。
2.1.spring-mvc-servlet.xml
- 通过
mvc:interceptors
定义Spring MVC拦截器 - 通过
mvc:mapping path
设置被拦截的路径 - 通过
mvc:exclude-mapping path
设置不拦截的路径 - 通过
bean class
装配拦截器
<!--拦截器-->
<mvc:interceptors>
<!--权限控制拦截器-->
<mvc:interceptor>
<!--对所有路径进行拦截-->
<mvc:mapping path="/**"/>
<!--不拦截登录页面-->
<mvc:exclude-mapping path="/login.jsp"/>
<!--不拦截登录方法-->
<mvc:exclude-mapping path="/login"/>
<bean class="pers.hanchao.hespringmvc.interceptors.interceptor.SessionCheckHandlerInterceptor"/>
</mvc:interceptor>
</mvc:interceptors>
2.2.SessionCheckHandlerInterceptor.java拦截器
- 通过校验
request
的头部header
信息的X-Requested-With
字段是否为XMLHttpRequest
来判断一个请求是否为ajax
请求。 - 继承
HandlerInterceptorAdapter
而不是直接实现HandlerInterceptor
,是因为继承HandlerInterceptorAdapter
只需要重写需要的切面方法。 - 拦截方法返回
true
,表示流程继续,会继续调用其他的拦截器;拦截方法返回false
,表示流程终止,不会再调用其他拦截器。
package pers.hanchao.hespringmvc.interceptors.interceptor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import pers.hanchao.hespringmvc.interceptors.JsonResult;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
/**
* 简单的权限校验拦截器,分别处理了ajax和非ajax请求
* Created by 韩超 on 2018/1/25.
*/
public class SessionCheckHandlerInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//获取session新
String username = (String) request.getSession().getAttribute("username");
//如果session为空,则跳转这登录页面
if (null == username || "".equals(username)){
//获取request头信息,判断是否是ajax请求
String headerAjax = request.getHeader("X-Requested-With");
//如果是ajax请求,则应该通过response.getWriter返回前端
if ("XMLHttpRequest".equals(headerAjax)){
//设置返回的ajax对象,并转化成String字符串
JsonResult jsonResult = new JsonResult(-1,"会话过期");
ObjectMapper objectMapper = new ObjectMapper();
String jsonStr = objectMapper.writeValueAsString(jsonResult);
//通过response写会前台
PrintWriter pw = response.getWriter();
pw.write(jsonStr);
pw.flush();
pw.close();
//方法返回false,表示流程终止,不会再调用其他拦截器
return false;
}else {//如果是普通请求,直接重定向至登录页面即可。
response.sendRedirect(request.getContextPath() + "/login.jsp");
}
}
//方法返回true,表示流程继续,继续调用其他拦截器
return true;
}
}
2.3.登录相关代码
User.java
用来存储用户信息
public class User {
private String username;
private String password;
//toString/setter/getter
}
JsonResult.java
用来返回json
信息。
public class JsonResult {
private Integer code = 1;
private String message = "success!";
//constructor/toString/setter/getter
}
LoginController.java
用来模拟登录登出
package pers.hanchao.hespringmvc.interceptors;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import javax.servlet.http.HttpServletRequest;
/**
* Created by 韩超 on 2018/1/23.
*/
@Controller
public class LoginController {
/**
* <p>Title: 模拟造数,用于登录session</p>
* @author 韩超@bksx 2018/1/24 17:54
*/
@ModelAttribute
public User init(){
User user = new User();
user.setUsername("张三");
user.setPassword("password");
return user;
}
/**
* <p>Title: 简单的登录演示</p>
* @author 韩超@bksx 2018/1/24 17:55
*/
@PostMapping("/login")
public String login(HttpServletRequest request, @ModelAttribute User user, Model model){
request.getSession().setAttribute("username",user.getUsername());
model.addAttribute("username",user.getUsername());
return "/index";
}
/**
* <p>Title: 简单的登出演示</p>
* @author 韩超@bksx 2018/1/24 17:55
*/
@PostMapping("/logout")
public String logout(HttpServletRequest request){
request.getSession().setAttribute("username","");
return "/login";
}
}
2.4.result
3.请求信息打印拦截器
实现思路:
1. 关于方法执行时间,需要首先重写preHandle
方法记录方执行前的时刻,然后重写afterCompetition
方法记录方法执行完的时间,从而计算得出方法执行的时间。
2. 关于请求的控制器(Controller
)、请求方法(method
)、请求类型(POST
/GET
等等)、请求URI
(/hello
)等信息,只需要重写preHandle
方法,在方法执行前,通过request
和handler
获取相关信息。
3. 关于请求参数,不能简略的只是重写preHandle
方法,从request.getInputStream
获取参数就可以了。因为request.getInputStream
只能获取一次参数,如果在拦截器里获取了这些参数并且不做其他处理,则到了方法的执行阶段,无法获取任何request
的body
部分的参数。为了解决这个问题,引入了Wrapper[包装器]
和Filter[过滤器]
技术,通过RequestBodyWrapper
包装器,将request
的body
数据取出来进行包装,使得request
能够多次被执行getInputStream
。通过RequestBodyWrapperFilter
实现request
的包装过程。
3.1.request的body信息包装器-RequestBodyWrapper
RequestBodyWrapper.java
通过继承HttpServletRequestWrapper
,重写getInputStream
和getReader
实现了对HttpServletRequest
请求的包装,将body
数据提取出来放入byte
数组中,从而实现request
中body
数据的多次使用。
package pers.hanchao.hespringmvc.interceptors.filter;
import org.apache.log4j.Logger;
import org.apache.log4j.lf5.util.StreamUtils;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
/**
* Created by 韩超 on 2018/1/25.
*/
public class RequestBodyWrapper extends HttpServletRequestWrapper {
//定义字段保存request的body信息
private byte[] requestBody;
private final static Logger LOGGER = Logger.getLogger(RequestBodyWrapper.class);
/**
* <p>Title: 读取request.getInputStream信息,保存到requestBody数组中</p>
* @author 韩超@bksx 2018/1/25 10:40
*/
public RequestBodyWrapper(HttpServletRequest request) {
super(request);
try {
//将request的body信息写入到requestBody中
requestBody = StreamUtils.getBytes(request.getInputStream());
} catch (IOException e) {
e.printStackTrace();
LOGGER.error("[URI=" + request.getRequestURI() + ",method=" + request.getMethod() + "],无法获取body数据!");
}
}
/**
* <p>Title: 重写getInputStream,返回requestBody数组中的信息</p>
* @author 韩超@bksx 2018/1/25 10:42
*/
@Override
public ServletInputStream getInputStream(){
if (null == requestBody){
requestBody = new byte[0];
}
final ByteArrayInputStream bais = new ByteArrayInputStream(requestBody);
return new ServletInputStream() {
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener readListener) {
}
@Override
public int read() throws IOException {
return bais.read();
}
};
}
/**
* <p>重写getReader方法,调用getInputStream,将byte[]数组转换成BufferedReader</p>
* @author hanchao 2018/1/26 14:54
**/
@Override
public BufferedReader getReader(){
return new BufferedReader(new InputStreamReader(getInputStream()));
}
}
3.2.对每次Request请求进行包装-RequestBodyWrapperFilter
RequestBodyWrapper
提供了对request
的body
数据进行包装的包装器,还需要通过过滤器实现对每一个request
请求的包装。RequestBodyWrapperFilter
继承了OncePerRequestFilter
,包装每个request
请求只包装一次。
package pers.hanchao.hespringmvc.interceptors.filter;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 为了包装request而创建的过滤器,实现了request.getInputStream的多次使用
* OncePerRequestFilter:spring封装的Filter,包装了每个request只被调用一次
* Created by 韩超 on 2018/1/25.
*/
public class RequestBodyWrapperFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
RequestBodyWrapper wrapperedRequest = new RequestBodyWrapper(httpServletRequest);
filterChain.doFilter(wrapperedRequest,httpServletResponse);
}
}
3.3.请求信息打印拦截器-RequestInfoHandlerInterceptor
- 通过分别记录
preHandle
和afterCompetition
的时间,计算出方法执行时间 - 通过
handler instanceof HandlerMethod
限定请求类型是方法请求,而不是静态资源等请求 - 通过
((HandlerMethod)Handler).getBean.getClass.getName()
获取控制器名 - 通过
((HandlerMethod)Handler).getMethod
获取方法全名 - 通过
request.getRequestURI()
获取URI
- 通过
request.getMethod()
获取请求类型 - 通过
request.getInputStream
获取request
请求的body
数据流 - 通过
BufferedReader
读取request
的body
数据流
package pers.hanchao.hespringmvc.interceptors.interceptor;
import org.apache.log4j.Logger;
import org.springframework.lang.Nullable;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.lang.reflect.Method;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* 请求信息拦截器:获取Controller名、method名、请求参数、URI、耗时等信息。
* 此拦截器需要HttpServletRequestBodyWrapper和HttpServletRequestBodyWrapperFilter的配合
* Created by 韩超 on 2018/1/25.
*/
public class RequestInfoHandlerInterceptor extends HandlerInterceptorAdapter {
private final static Logger LOGGER = Logger.getLogger(RequestInfoHandlerInterceptor.class);
private ThreadLocal<Long> startTime = new ThreadLocal<Long>();
/**
* <p>Title: 打印请求的控制器、方法、请求类型、请求参数、请求时间等信息</p>
* @author 韩超@bksx 2018/1/25 10:49
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//如果是方法请求,则进行统计
if (handler instanceof HandlerMethod){
//获取方法执行前时间
Long now = System.currentTimeMillis();
startTime.set(now);
//格式化当前时间,以便输出
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String methodTime = sdf.format(new Date(now));
//获取Controller
String controller = ((HandlerMethod) handler).getBean().getClass().getName();
//获取方法
Method mehtod = ((HandlerMethod) handler).getMethod();
//获取URI
String uri = request.getRequestURI();
//获取请求类型 POST 、PUT 、GET等等
String type = request.getMethod();
//获取请求参数
//将request的body信息放到缓存读取器中
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(request.getInputStream(),"UTF-8"));
//创建缓存字符串存储request的body信息
StringBuffer stringBuffer = new StringBuffer();
//将bufferedReader的数据读取到stringBuffer中
String line;
while (null != (line = bufferedReader.readLine())){
stringBuffer.append(line);
}
String parameters = stringBuffer.toString();
//组合信息
StringBuffer sb = new StringBuffer();
sb.append("\n--------------------------------------------------------------------------------");
sb.append(methodTime);
sb.append("--------------------------------------------------------------------------------");
sb.append("\n----------Controller :").append(controller);
sb.append("\n----------Method :[").append(mehtod);
sb.append("\n----------Parameters :").append(parameters);
sb.append("\n----------URI :[").append(uri);
sb.append("\n--------------------------------------------------------------------------------");
sb.append("-------------------");
sb.append("--------------------------------------------------------------------------------");
LOGGER.info(sb.toString());
}
return true;
}
/**
* <p>Title:在Handler完成 handle之后,获取此时的系统时间,计算出整个handle用时 </p>
* @author 韩超@bksx 2018/1/25 11:17
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
//如果是方法请求,则进行统计
if (handler instanceof HandlerMethod){
//获取方法用时
Long now = System.currentTimeMillis();
Long useTime = now - startTime.get();
//格式化当前日期
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String endTime = sdf.format(new Date(now));
//获取URI
String uri = request.getRequestURI();
//获取请求类型 POST 、PUT 、GET等等
String type = request.getMethod();
//组合信息
StringBuffer sb = new StringBuffer();
sb.append("========{").append(endTime).append(",URI = [").append(uri).append("],method = ").append(true)
.append(",").append("Use Time : ").append(useTime).append(" 毫秒}\n");
LOGGER.info(sb.toString());
}
}
}
3.4.spring-mvc-servlet.xml
<!--方法执行信息拦截器-->
<mvc:interceptor>
<mvc:mapping path="/**"/>
<bean class="pers.hanchao.hespringmvc.interceptors.interceptor.RequestInfoHandlerInterceptor"/>
</mvc:interceptor>
3.5.result
三种类型的请求日志:无参数、GET请求、POST请求
2018-01-26 15:37:12 INFO RequestInfoHandlerInterceptor:75 -
--------------------------------------------------------------------------------2018-01-26 15:37:12--------------------------------------------------------------------------------
----------Controller :pers.hanchao.hespringmvc.interceptors.LoginController
----------Method :[public java.lang.String pers.hanchao.hespringmvc.interceptors.LoginController.login(javax.servlet.http.HttpServletRequest,pers.hanchao.hespringmvc.interceptors.User,org.springframework.ui.Model)
----------Parameters :
----------URI :[/login
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
2018-01-26 15:37:12 INFO RequestInfoHandlerInterceptor:103 - ========{2018-01-26 15:37:12,URI = [/login],method = true,Use Time : 3 毫秒}
2018-01-26 15:37:18 INFO RequestInfoHandlerInterceptor:75 -
--------------------------------------------------------------------------------2018-01-26 15:37:18--------------------------------------------------------------------------------
----------Controller :pers.hanchao.hespringmvc.requestannotation.RequestAnnotationController
----------Method :[public java.lang.String pers.hanchao.hespringmvc.requestannotation.RequestAnnotationController.postRequestParam(java.lang.String,org.springframework.ui.Model)
----------Parameters :postname=%E6%9D%8E%E5%9B%9B
----------URI :[/requestannotation/postrequestparam
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
2018-01-26 15:37:18 INFO RequestInfoHandlerInterceptor:103 - ========{2018-01-26 15:37:18,URI = [/requestannotation/postrequestparam],method = true,Use Time : 5 毫秒}
2018-01-26 15:37:42 INFO RequestInfoHandlerInterceptor:75 -
--------------------------------------------------------------------------------2018-01-26 15:37:42--------------------------------------------------------------------------------
----------Controller :pers.hanchao.hespringmvc.requestannotation.RequestAnnotationController
----------Method :[public pers.hanchao.hespringmvc.requestannotation.User pers.hanchao.hespringmvc.requestannotation.RequestAnnotationController.postRequestBody(pers.hanchao.hespringmvc.requestannotation.User)
----------Parameters :{"name":"张三","sex":"男"}
----------URI :[/requestannotation/requestbody
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
2018-01-26 15:37:42 INFO RequestInfoHandlerInterceptor:103 - ========{2018-01-26 15:37:42,URI = [/requestannotation/requestbody],method = true,Use Time : 20 毫秒}