一、Java过滤器和拦截器
1.1、过滤器(Filter)
Filter过滤器,是Servlet(Server Applet)技术中的技术,开发人员可以通过Filter技术,管理web资源,可以对指定的一些行为进行拦截,例如URL级别的权限访问控制等。在ServletRequest到达Servlet之前,拦截客户端的ServletRequest,可以根据需要检查ServletRequest,也可以修改ServletRequest中的头和数据;在ServletResponse到达客户端之前,可以拦截ServletResponse,可以根据需要检查ServletResponse,同样也可以修改。
通过实现Filter接口(属于javax.servlet包),实现doFilter()方法,即可自定义过滤器。
1.2、拦截器(Interceptor)
Interceptor拦截器是Spring MVC框架中对请求进行拦截和处理的组件,可以实现权限验证、日志记录、异常处理等功能,拦截器是在Spring MVC框架中执行的。在HttpServletRequest到达Controller之前,可以根据需要检查HttpServletRequest,也可以进行修改;在HttpServletResponse返回之前,可以根据需要检查HttpServletResponse,也可以进行修改。
通过实现HandlerInterceptor接口(属于org.springframework.web.servlet包),实现preHandle(前置)、postHandle(后置)、afterCompletion(完成后)方法,即可自定义拦截器。
1.3、两者区别
相同点:
1、拦截器和过滤器都体现了AOP的思想,都可以拦截请求方法;
2、过滤器和拦截器可以定义多个,且可以通过Order自定义执行顺序;
不同点:
1、运行位置不同:过滤器运行在Web服务器和Servlet容器之间,拦截器是针对具体控制器方法前进行拦截;
2、功能不同:过滤器主要对请求进行预处理或者过滤;而拦截器主要对请求进行流程控制;
3、依赖框架不同:过滤器是基于Servlet实现的,而拦截器是基于Spring MVC的。
二、代码应用
2.1、需求说明
通过过滤器实现初步过滤,筛除掉一些非法IP访问,例如不允许本机(127.0.0.1)外的所有IP地址访问。通过拦截器实现登录信息或身份信息校验,并实现日志记录功能。
2.2、过滤器编写
import com.hao.dockertest.util.MyTimeUtil;
import lombok.extern.slf4j.Slf4j;
import javax.servlet.*;
import java.io.IOException;
import java.io.PrintWriter;
/**
* @author Hao
* @program: DockerTest
* @description: 过滤器
* @date 2023-10-21 21:11:28
*/
@Slf4j
public class MyFilter implements Filter {
@Override
public void init(javax.servlet.FilterConfig filterConfig) throws ServletException {
log.info("Filter init!");
Filter.super.init(filterConfig);
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
log.info("Filter Start! 实现初步过滤");
// 从ServletRequest获取相关信息
String ip = servletRequest.getRemoteAddr();
if (ip.equals("0:0:0:0:0:0:0:1")) ip = "127.0.0.1";
String time = MyTimeUtil.sdf.format(System.currentTimeMillis());
log.info("访问IP:{},访问时间:{}", ip, time);
// 拦截非本机IP
if(!ip.equals("127.0.0.1")){
log.error("非本机IP({})禁止访问!", ip);
// 自定义ServletResponse,返回前端提示信息
PrintWriter printWriter = servletResponse.getWriter();
servletResponse.setCharacterEncoding("UTF-8");
servletResponse.setContentType("application/json;charset=UTF-8");
printWriter.write("Sorry, your IP address does not allow access!");
return;
}
// 手动放行(如果上面加了过滤条件,最后必须手动放行!)
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void destroy() {
log.info("Filter destroy!");
javax.servlet.Filter.super.destroy();
}
}
注册自定义的过滤器
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;
/**
* @author Hao
* @program: DockerTest
* @description: 定义过滤器
* @date 2023-10-21 21:18:05
*/
@Configuration
public class FilterConfig {
@Bean
public FilterRegistrationBean<Filter> filterRegistrationBean(){
FilterRegistrationBean<Filter> filterFilterRegistrationBean = new FilterRegistrationBean<>();
// 注册自定义的过滤器
filterFilterRegistrationBean.setFilter(new MyFilter());
return filterFilterRegistrationBean;
}
}
实现效果:
可以看到非本地IP的访问已经被拦截了。
本地IP的访问可以被正常放行。
2.3、拦截器编写
import com.hao.dockertest.util.JWTUtil;
import com.hao.dockertest.util.MyTimeUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
/**
* @author Hao
* @program: DockerTest
* @description: 拦截器
* @date 2023-10-21 21:13:27
*/
@Slf4j
@Configuration
public class MyInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("拦截器前置处理 preHandle");
// 验证token
String token = request.getHeader("token");
if(token == null || !JWTUtil.checkToken(token)){
log.error("未登录或身份信息验证失败");
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json;charset=UTF-8");
PrintWriter printWriter = response.getWriter();
printWriter.write("您还未登录或身份验证失败,请重新登录!");
return false;
}
return HandlerInterceptor.super.preHandle(request, response, handler);
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
log.info("拦截器后置处理 postHandle");
// 从HttpServletRequest获取相关信息
String ip = request.getRemoteAddr();
if (ip.equals("0:0:0:0:0:0:0:1")) ip = "127.0.0.1";
String browser = request.getHeader("Sec-Ch-Ua-Platform");
String httpMethod = request.getMethod();
String time = MyTimeUtil.sdf.format(System.currentTimeMillis());
// 日志存档
// Record record = new Record(null, ip, httpMethod, browser, time);
// 访问日志记录逻辑
// 从token中获取相关信息
String token = request.getHeader("token");
String userName = JWTUtil.getTokenInfo(token, "userName");
String userId = JWTUtil.getTokenInfo(token, "userId");
log.info("访问用户:{}-{},访问IP:{},访问时间:{},请求方式:{},访问设备:{}",userName, userId, ip, time, httpMethod, browser);
HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
log.info("拦截器完成后 afterCompletion");
HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
}
}
配置自定义的拦截器
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;
/**
* @author Hao
* @program: DockerTest
* @description: Interceptor配置
* @date 2023-10-21 21:26:18
*/
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Autowired
private MyInterceptor myInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(myInterceptor).addPathPatterns("/**").excludePathPatterns("/login"); // 拦截所有请求,排除login
}
}
2.4、JWT工具
首先引入JWT工具jjwt的pom依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
自定义JWT工具类,包含构建Token,检验token有效性和解析token。
import io.jsonwebtoken.*;
import lombok.extern.slf4j.Slf4j;
import java.util.Date;
import java.util.UUID;
/**
* @author Hao
* @program: DockerTest
* @description: JWT工具
* @date 2023-10-23 10:44:36
*/
@Slf4j
public class JWTUtil {
private static long time = 1000*60*60*10;
// 签名
private static final String signature = "test";
public static String createToken(String userName, String userID){
JwtBuilder jwtBuilder = Jwts.builder();//构建JWT对象
return jwtBuilder
// Header
.setHeaderParam("typ","JWT")
.setHeaderParam("alg","HS256")
// payload
.claim("userName",userName)
.claim("userId", userID)
// 设置有效期(毫秒单位)
.setExpiration(new Date(System.currentTimeMillis()+time))
.setId(UUID.randomUUID().toString())
// signature
.signWith(SignatureAlgorithm.HS256, signature)
// compact拼接三部分header、payload、signature
.compact();
}
public static Boolean checkToken(String token){
if(token == null){
return false;
}
try {
JwtParser jwtParser = Jwts.parser();
jwtParser.setSigningKey(signature).parseClaimsJws(token);
return true;
}catch (Exception e){
// log.error("token失效");
return false;
}
}
public static String getTokenInfo(String token, String key){
if(token == null || key == null) return null;
JwtParser parser = Jwts.parser();
Jws<Claims> claimsJws = parser.setSigningKey(signature).parseClaimsJws(token);
Claims payload = claimsJws.getBody();
// 获取key对于的内容
return payload.get(key).toString();
}
}
2.5、模拟Controller
主要模拟访问业务和登录业务
import com.hao.dockertest.util.JWTUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
/**
* @author Hao
* @program: DockerTest
* @description: 模拟Controller
* @date 2023-10-20 12:13:28
*/
@RestController
@RequestMapping
@Slf4j
public class RecordController {
@GetMapping
public String getInfo(){
return "Welcome!";
}
@PostMapping("/login")
public String login(){
return JWTUtil.createToken("张三", "20210919"); // 模拟返回登录成功token
}
}
其中getinfo()为模拟某个业务,需要登陆之后才能访问;而login()是登录方法,返回给前端token,这个方法拦截器默认放行。
2.6、测试
我们使用PostMan模拟一下拦截器功能
可以看到,过滤器先启动,初步过滤掉非法IP,然后拦截器启动,验证token信息,在postman的Headers中我们并未设置token,拦截器检查到HttpServletRequest不包含token,会返回前端错误信息,提示用户登录。
然后我们访问login:
可以看到这里只有过滤器起了作用,因为我们拦截器设置了排除/login这个路径,因此/login不会被拦截器拦截,也就可以正常的访问controller中的login方法,在postman中我们可以看到已经正常访问,并返回了token。
将返回的token,我们手动加入到Headers的token中,再访问一次上面模拟的业务逻辑:
可以看到已经可以正常访问,再看一下控制台日志:
可以看到拦截器正常拦截,并从中获取到了关键信息,打印了日志,并成功放行,基于此就实现了基于拦截器的身份验证功能。
此外,我们还可以模拟一下token失效或者被非法篡改,看一下拦截效果。我们随机把Headers的token删除几个字符,再试一下访问效果。
可以看到,即使携带token,但token已经实现,就无法对controller中的方法进行访问,被拦截器成功拦截。
三、应用场景
常见的过滤器用途主要包括:对用户请求进行统一认证、对用户访问的请求进行记录和审核,对用户发送的数据进行过滤或替换,转换图像格式、对响应内容进行压缩,对请求或响应进行加密解密等。
拦截器采用了AOP的设计思想,可以用来拦截处理方法在之前和之后执行的一些跟主业务没有关系的公共功能,例如权限控制,日志、异常记录,记录方法执行时间等。