拦截器(Interceptor)是一种在应用程序中用于干预、修改或拦截请求和响应的组件,是AOP 编程的一种实践,和过滤器一样都是一种具体的AOP实现。它可以在请求被发送到目标处理程序之前或之后,对请求进行预处理或对响应进行后处理。
拦截器通常用于以下目的:
认证和授权:拦截器可以对请求进行身份验证和权限检查,确保只有经过认证和授权的用户才能访问特定的资源或执行特定的操作。
日志记录和性能监控:拦截器可以用于记录请求和响应的日志,以便进行故障排查、性能监控和分析。
请求转发和重定向:拦截器可以根据特定的条件,对请求进行转发或重定向至不同的处理程序或页面。
数据转换和格式化:拦截器可以对请求和响应的数据进行转换、格式化或验证,以满足特定的需求或规范。
在很多框架和应用程序中都有拦截器的概念,比如在Java中的Spring MVC框架中,可以使用拦截器来拦截和处理HTTP请求;在网络安全领域,防火墙和入侵检测系统也可以使用拦截器来拦截和过滤恶意流量。拦截器在应用程序中起到了很重要的作用,增强了系统的安全性、可靠性和可控性。
1.拦截器的编写方法
拦截器的编写比较简单,一般是根据自己的不同需要继承不同的拦截器基类。大致分为两类,一类是基于业务判断服务的拦截器,比如:日志服务,权限认证,这种拦截器一般继承自 HandlerInterceptor类;另一类是和配置有关的拦截器,一般继承自 WebMvcConfigurer 。其中第一类最多,需要重写 preHandle 和 postHandle 方法,其中 preHandle 方式用于请求执行前的时候,当preHandle 返回 true 才能进行下一个方法,而 postHandle 是作用于请求结束前。afterCompletion 是视图显示完成后才会执行。
第一类,基于 HandlerNterceptor 类的拦截器 ,用于日志服务:
java
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
@Component
public class LoggerInterceptor extends HandlerInterceptorAdapter {
//生成日志类
private Logger logger = LoggerFactory.getLogger(LoggerInterceptor.class);
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HandlerMethod handlerMethod = (HandlerMethod) handler;
// Returns the name of the HTTP method with which this request was made, for example, GET, POST, or PUT.
Method method = handlerMethod.getMethod();//得到方法信息
//从当前方法中获取一个日志注解对象
LogAnnotation logAnnotation = method.getAnnotation(LogAnnotation.class);
if (logAnnotation != null){
long startTime = System.currentTimeMillis();
request.setAttribute("startTime",startTime);//设置属性
logger.info("enter"+method.getName()+"method cost time is:"+startTime+" ms");
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();//得到方法信息
LogAnnotation logAnnotation = method.getAnnotation(LogAnnotation.class);
if (logAnnotation != null){
long endTime = System.currentTimeMillis();
long startTime = (long) request.getAttribute("startTime");
long periodTime = endTime- startTime;
logger.info("Leave "+method.getName()+"method time is:"+endTime+" ms");
logger.info("On "+method.getName()+"method cost time is:"+periodTime+" ms");
}
}
}
将分别计算并输出进入某个方法和离开某个方法的时间。
声明注解 :在需要使用该服务的方法上加上该注解即可使用
java
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(value = RetentionPolicy.RUNTIME)
public @interface LogAnnotation {
String message() default "666";
String[] params() default {};
}
注册配置拦截器:实现WebMvcConfigurer接口,并重写addInterceptors方法
java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoggerInterceptor loggerInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loggerInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/ls");//不拦截的路径
}
}
2.邮箱有效性验证
常见的邮箱如 qq,网易,地址形式如 xxx@qq.com ,xxx@163.com .
总结成正则表达式如下:
"^([a-z0-9A-z]+[-|\\.]?)+[a-z0-9A-z]@([a-z0-9A-Z]+(-[a-z0-9A-Z]+)?\\.)+([a-zA-Z]{2,})$"
用Java编写一个检查有效性的例子:
java
public static boolean matchTest(String email){
boolean a = email.matches("^([a-z0-9A-Z]+[-|\\.]?)+[a-z0-9A-Z]@([a-z0-9A-Z]+(-[a-z0-9A-Z]+)?\\.)+([a-zA-Z]{2,})$");
System.out.println(a);
return a;
}
编写完整的拦截器:
java
import org.example.service.Testjson;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
public class EmailInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String email = request.getParameter("email");
if (Testjson.matchTest(email)){
return true;
}else {
return false;
}
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}
这里只需要重写 preHandle 方法即可,完成对有效性邮箱地址的判断效果。
3.Token 检查拦截器实现
Token检查是一个健全系统或者服务必须有的一项安全性功能,特别是基于 RESTful 的接口,需要检查Token来判断用户是否已经登陆或者验证身份,利用Token可以获取进一步的网络资源和权限。
一般我们会使用 JWT 验证方式,JSON Web Token 简称 JWT。本质上是一段字符串,由以下三个部分构成。
- 头部: JWT 基本信息,包括算法。
- 荷载: 自定义数据,包括用户标识,如 userid。
- 签名: 前面两者通过一定加密算法后计算所得其签名字符串。
引入 JWT 依赖到 pom.xml 文件中,代码如下:
XML
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.0</version>
</dependency>
更新前端拦截器配置,处理所有请求:
java
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoggerInterceptor loggerInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loggerInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/ls");//不拦截的路径
registry.addInterceptor(authenticationInterceptor())
.addPathPatterns("/**");
}
@Bean
public AuthenticationInterceptor authenticationInterceptor(){
return new AuthenticationInterceptor();
}
}
新建两个注解,用于判断是否进行Token验证,首先编写需要登陆验证的注解UserLoginToken:
java
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 需要登陆才能操作得注解
*/
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface UserLoginToken {
boolean required() default true;
}
用于跳过验证的注解:
java
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface PassToken {
boolean required() default true;
}
检查Token的拦截器:
java
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import org.example.Exception.BusinessException;
import org.example.pojo.User;
import org.example.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
@Component
public class AuthenticationInterceptor implements HandlerInterceptor {
@Autowired
UserService userService;
@Autowired
RedisUtil redisUtil;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader("token");
//排除访问的是静态资源,而不是映射访问
if (!(handler instanceof HandlerMethod)){
return true;
}
//获取访问的方法
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
//是否被跳过验证注解
if (method.isAnnotationPresent(PassToken.class)){
PassToken passToken = method.getAnnotation(PassToken.class);
if (passToken.required()){
return true;
}
}
if (method.isAnnotationPresent(UserLoginToken.class)){
UserLoginToken userLoginToken = method.getAnnotation(UserLoginToken.class);
if (userLoginToken.required()){
if (token == null){
throw new BusinessException("4001","no token");
}
String userId;
try{
userId = JWT.decode(token).getAudience().get(0);
}catch (Exception e){
throw new BusinessException("4003","decode token fails");
}
//Check the expire of token
String tokenKey = userId+":"+token;
boolean hasExisted = redisUtil.hasKey(tokenKey);
System.out.println("exit or not: "+hasExisted);
if (!hasExisted){
throw new BusinessException("4005","token expired!");
}
int userID = Integer.parseInt(userId);
System.out.println("userId is:"+userID);
User user =userService.findUserById(userID);
if (user == null){
throw new RuntimeException("user not exits");
}
try {
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(user.getPassword()+"MTest!76&sQ^")).build();
jwtVerifier.verify(token);
//设置当前登录用户
LoginUser loginUser = new LoginUser();
loginUser.setID((long)userID);
UserContext.setUser(loginUser);
}catch (JWTVerificationException e){
throw new BusinessException("4002","invalid token");
}
}
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}
使用起来也很方便,在需要与不需要进行 Token 验证的接口前面加上对应的注释即可。
补充:
handler
参数是 Spring MVC 框架传递给你的preHandle
方法的一个对象,它代表当前请求即将要调用的控制器逻辑。在 Spring MVC 中,这个handler
对象可以是多种类型,但通常它是HandlerMethod
的一个实例。当一个请求被映射到某个控制器的方法时,这个方法就被包装为
HandlerMethod
对象。HandlerMethod
对象包含了这个方法以及包含这个方法的控制器的信息,允许你在拦截器中对即将执行的控制器方法进行一些前处理。在某些情况下,
handler
可能不是HandlerMethod
的实例。例如,当请求是针对静态资源时,或者是某个请求没有被映射到任何的控制器上的方法时,handler
可能是框架中其他类型的处理器,这取决于你的应用配置和请求的具体类型。这就是为什么在preHandle
方法中进行instanceof HandlerMethod
检查是一种常见的做法,以确保你正在处理的是一个具体的控制器方法。如果是,则可以安全地转型到HandlerMethod
并进行进一步的处理。在
preHandle
方法的上下文中,你可以对handler
参数进行各种检查和操作。例如,你可以检查用户是否有权限执行与handler
对应的控制器方法,你可以修改请求或响应,或者决定是否继续执行请求处理链(通过返回true
或者false
)。
在Spring MVC框架中:静态资源请求:
这些请求通常不需要经过复杂的处理,它们被映射到服务器上存储静态文件的目录。例如在Spring Boot中,静态资源可以放置在 `/static`、`/public` 等默认的资源目录中,这些目录中的资源可以直接通过URL被访问。在这种情况下,与静态资源对应的`handler`可能就不是一个`HandlerMethod`,而是其他用于处理静态资源的类的实例。
数据请求:
这些请求需要后端处理,如执行数据库操作、进行计算或调用其他接口等。在Spring MVC中,这些请求被映射到控制器类中的特定方法上,这些方法通过注解(如`@RequestMapping`或其特化版本比如`@GetMapping`、`@PostMapping`等)来标注。这时候传递给`preHandle`方法的`handler`参数就是一个`HandlerMethod`实例,代表被映射的那个方法。
因此,在拦截器的`preHandle`方法中,进行`handler instanceof HandlerMethod`检查可以帮助你明确该次请求是需要Spring MVC通过控制器中的方法处理,还是仅仅是为了获取静态资源。这可以确保你的拦截器逻辑只应用于实际需要拦截的后端处理请求,而不是静态资源请求。