Springboot下使用拦截器(Interceptor)和 过滤器(Filter)

  • Post author:
  • Post category:其他




Springboot下使用拦截器(Interceptor)和 过滤器(Filter)



1.拦截器(Interceptor)



1.1 工作原理

一个拦截器,只有preHandle方法返回true,postHandle、afterCompletion才有可能被执行;如果preHandle方法返回false,则该拦截器的postHandle、afterCompletion必然不会被执行。拦截器不是Filter,却实现了Filter的功能,其原理在于:

  • 所有的拦截器(Interceptor)和处理器(Handler)都注册在HandlerMapping中。
  • Spring MVC中所有的请求都是由DispatcherServlet分发的。
  • 当请求进入DispatcherServlet.doDispatch()时候,首先会得到处理该请求的Handler(即Controller中对应的方法)以及所有拦截该请求的拦截器。拦截器就是在这里被调用开始工作的。



1.2 拦截器工作流程

  • 正常流程:

    Interceptor.preHandle >> Controller处理请求 >> Interceptor.postHandle >> 渲染视图view >> Interceptor.afterCompletion

  • 中断流程:

    如果在Interceptor1.preHandle中报错或返回false ,那么接下来的流程就会被中断,但注意被执行过的拦截器的afterCompletion仍然会执行。下图为Interceptor1.preHandle返回false的情况:

    前置拦截器2 preHandle: 用户名:null

    前置拦截器1 preHandle: 请求的uri为:http://localhost:8010/user/353434

    拦截器2 afterCompletion:



1.3 和过滤器(Filter)共存时的执行顺序

拦截器是在DispatcherServlet这个servlet中执行的,因此所有的请求最先进入Filter,最后离开Filter。其顺序如下。

Filter->Interceptor.preHandle->Handler->Interceptor.postHandle->Interceptor.afterCompletion->Filter



1.4 应用场景

拦截器本质上是面向切面编程(AOP),符合横切关注点的功能都可以放在拦截器中来实现,主要的应用场景包括:

  • 登录验证,判断用户是否登录。

  • 权限验证,判断用户是否有权限访问资源,如校验token

  • 日志记录,记录请求操作日志(用户ip,访问时间等),以便统计请求访问量。

  • 处理cookie、本地化、国际化、主题等。

  • 性能监控,监控请求处理时长等。



1.5 实际使用

通过拦截器防止用户暴力请求连接,使用用户IP来限制访问次数

主要逻辑:记录用户IP访问次数,第一次访问时在redis中创建一个有效时长1秒的key,当第二次访问时key值+1,当值大于等于5时在redis中创建一个5分钟的key,当拦截器查询到reids中有当前IP的key值时返回false限制用户请求接口



1.5.1 实现拦截器

实现拦截器可以通过继承HandlerInterceptorAdapter类。如果preHandle方法return true,则继续后续处理。

/**
 * @Author: Xujh
 * ip+url重复请求现在拦截器
 */
@Slf4j
public class IpUrlLimitInterceptor implements HandlerInterceptor {
    @Autowired
    private RedisUtils redisUtils;

    private static final String LOCK_IP_URL_KEY="lock_ip_";

    private static final String IP_URL_REQ_TIME="ip_url_times_";

    //访问次数限制
    private static final long LIMIT_TIMES=5;

    //限制时间 秒为单位
    private static final int IP_LOCK_TIME=300;

    @Override
    public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception {
        log.info("request请求地址uri={},ip={}", httpServletRequest.getRequestURI(), IpUtils.getRealIP(httpServletRequest));
        if (ipIsLock(IpUtils.getRealIP(httpServletRequest))){
            log.info("ip访问被禁止={}",IpUtils.getRealIP(httpServletRequest));
            returnJson(httpServletResponse, JSON.toJSONString(AppResponse.onError("当前操作过于频繁,请5分钟后重试")));
            return false;
        }
        if(!addRequestTime(IpUtils.getRealIP(httpServletRequest),httpServletRequest.getRequestURI())){
            returnJson(httpServletResponse, JSON.toJSONString(AppResponse.onError("当前操作过于频繁,请5分钟后重试")));
            return false;
        }
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {

    }

    /**
     * @Description: 判断ip是否被禁用
     * @param ip
     * @return java.lang.Boolean
     */
    private Boolean ipIsLock(String ip){

        if(redisUtils.hasKey(LOCK_IP_URL_KEY+ip)){
            return true;
        }
        return false;
    }
    /**
     * @Description: 记录请求次数
     * @param ip
     * @param uri
     * @return java.lang.Boolean
     */
    private Boolean addRequestTime(String ip,String uri){
	
        String key=IP_URL_REQ_TIME+ip+uri;
        if (redisUtils.hasKey(key)){
            long time=redisUtils.incr(key,(long)1);
            if (time>=LIMIT_TIMES){
                redisUtils.set(LOCK_IP_URL_KEY+ip,ip,IP_LOCK_TIME);
                return false;
            }
        }else {
            redisUtils.set(key,(long)1,1);
        }
        return true;
    }

    private void returnJson(HttpServletResponse response, String json) throws Exception {
        PrintWriter writer = null;
        response.setCharacterEncoding("UTF-8");
        response.setContentType("text/json; charset=utf-8");
        try {
            writer = response.getWriter();
            writer.print(json);
        } catch (IOException e) {
            log.error("LoginInterceptor response error ---> {}", e.getMessage(), e);
        } finally {
            if (writer != null) {
                writer.close();
            }
        }
    }

下面这个是获取用户IP的方式(感觉不太准确,存在问题,希望大佬看到可以提供个更好的方法)

/**
 * @Author: Xujh
 * @Date: 2021/4/7 14:30
 */
@Slf4j
public class IpUtils {
    /**
     * 获取用户真实IP地址,不使用request.getRemoteAddr()的原因是有可能用户使用了代理软件方式避免真实IP地址,
     * 可是,如果通过了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP值
     *
     * @return ip
     */
    public static String getRealIP(HttpServletRequest request) {
        String ip = request.getHeader("x-forwarded-for");
        if (ip != null && ip.length() != 0 && !"unknown".equalsIgnoreCase(ip)) {
            // 多次反向代理后会有多个ip值,第一个ip才是真实ip
            if( ip.indexOf(",")!=-1 ){
                ip = ip.split(",")[0];
            }
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
            log.info("Proxy-Client-IP ip: " + ip);
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
            log.info("WL-Proxy-Client-IP ip: " + ip);
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_CLIENT_IP");
            log.info("HTTP_CLIENT_IP ip: " + ip);
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_X_FORWARDED_FOR");
            log.info("HTTP_X_FORWARDED_FOR ip: " + ip);
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("X-Real-IP");
            log.info("X-Real-IP ip: " + ip);
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
            log.info("getRemoteAddr ip: " + ip);
        }
        return ip;
    }
}



1.5.2 注册拦截器

实现拦截器后还需要将拦截器注册到spring容器中,可以通过implements WebMvcConfigurer,覆盖其addInterceptors(InterceptorRegistry registry)方法。记得把Bean注册到Spring容器中,可以选择@Component 或者 @Configuration。

@Configuration
public class AdminConfig extends WebMvcConfigurationSupport {
/*
    @Autowired
    private AdminAdapter adminAdapter;*/
    @Bean
    IpUrlLimitInterceptor getIpUrlLimitInterceptor(){
        return new IpUrlLimitInterceptor();
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry){
        registry.addInterceptor(getIpUrlLimitInterceptor()).addPathPatterns("/**");
        super.addInterceptors(registry);
    }

}



2.过滤器(Filter)

springboot下过滤器的使用有两种形式:



2.1 注解形式

创建一个Filter,并使用WebFilter注解进行修饰,表示该类是一个Filter,以便于启动类进行扫描的时候确认

@WebFilter(urlPatterns = "/*",filterName = "filter2")
public class FilterAnnotationTest implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("过滤器2开始初始化");
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println("过滤器2开始工作");
        filterChain.doFilter(servletRequest,servletResponse);
    }

    @Override
    public void destroy() {
        System.out.println("过滤器2销毁");
    }
}

然后在启动类上添加注解@ServletComponentScan,该注解用于自动扫描指定包下(默认是与启动类同包下)的WebFilter/WebServlet/WebListener等特殊类。



2.2 代码注册方式

同样编写Filter,但是不添加WebFilter注解,通过@Bean注入spring

public class FilterDemo implements Filter {

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
        System.out.println("过滤器开始工作。。"+httpServletRequest.getRequestURL());
        filterChain.doFilter(servletRequest,servletResponse);
    }

    @Override
    public void destroy() {
        System.out.println("过滤器开始销毁");
    }
}

然后利用filterRegistrationBean来进行注册。也可以在代码里设置Order

@Configuration
public class FilterDemo {
    @Bean
    @Order(2)
    //spring boot会按照order值的大小,从小到大的顺序来依次过滤
    public FilterRegistrationBean configFilter(){
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
        filterRegistrationBean.setFilter(new FilterDemo());
        filterRegistrationBean.addUrlPatterns("/*");
        filterRegistrationBean.setName("sessionFilter");
        //filterRegistrationBean.setOrder(2);
        return filterRegistrationBean;
    }
    
}



2.3 过滤器(Filter)和拦截器(Interceptor)的区别

spring的拦截器和servlet的过滤器有相似之处,都是AOP思想的体现,都可以实现权限检查,日志记录,不同的是

  1. 适用范围不同:Filter是Servlet容器规定的,只能使用在servlet容器中,而拦截器的使用范围就大得多

  2. 使用的资源不同:拦截器是属于spring的一个组件,因此可以使用spring的所有资源,对象,如service对象,数据源,事务控制等,而过滤器就不行

  3. 深度不同:Filter还在servlet前后起作用。而拦截器能够深入到方法前后,异常抛出前后,因此拦截器具有更大的弹性,所有在spring框架中应该优先使用拦截器。

    通过调试可以发现,拦截器的执行过程是在过滤器的doFilter中执行的,过滤器的初始化会在项目启动时执行。

    ————————————————

    版权声明:本文为CSDN博主「余生之君」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。

    原文链接:https://blog.csdn.net/java_collect/article/details/80873686

执行顺序:

  1. 过滤器开始工作。。http://localhost:8010/user/353434
  2. 前置拦截器2 preHandle: 用户名:null
  3. 前置拦截器1 preHandle: 请求的uri为:http://localhost:8010/user/353434
  4. 拦截器1 postHandle:
  5. 拦截器2 postHandle:
  6. 拦截器1 afterCompletion:
  7. 拦截器2 afterCompletion:
  8. 过滤器开始工作。。http://localhost:8010/favicon.ico

个人笔记

菜鸡上路,望各位大佬多多帮助


本文主要借鉴与 CSDN博主「余生之君」



原文链接:https://blog.csdn.net/java_collect/article/details/80873686



版权声明:本文为weixin_44284923原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。