《Spring Boot 实战纪实》之过滤器

导航

  • 什么是过滤器
  • Spring的过滤器
    • Filter定义
    • 过滤的对象
    • 典型应用
  • 过滤器的使用
    • Filter生命周期
  • 过滤器链
  • 自定义敏感词过滤器
    • 新增自定义过滤器
    • 添加 @WebFilter注解
    • 添加 @ServletComponentScan 注解
  • 测试用例
  • 结语
  • 源码地址
  • 参考

本节是智客工坊-《Spring Boot 实战纪实》的第16篇,感谢您的阅读,预计阅读时长5min。

通俗的讲,过滤器可以简单理解为“取你所想取”,忽视掉那些你不想要的东西。



什么是过滤器

生活中有很多"过滤器"的例子。

一台带有过滤功能的饮水机,里面就会有个过滤器,可以将污水中的杂质过滤,从而使进入的污水变成净水。

渔民捕鱼使用的渔网,也有讲究,网眼尺寸要恰到好处,既要捕捞的捕猎到成鱼,又要给给小鱼苗一个逃生的机会。

建筑工地上常见的筛网,通常用于将沙子中细沙过滤...

Spring的过滤器也是一样,我们可以自己在一个没有过滤器的程序里面加上过滤器,同时可以选择拦截下什么资源,放行什么资源。

Spring的过滤器

Filter定义

一个驻留在服务端的web组件,可以截取用户端和资源之间的请求与响应信息,并对这些信息过滤。



  • 当Web容器接收到一个对的资源的请求时,它将判断是否有过滤器(可以有多个过滤器)与这个资源有关联

  • 如果有,容器把请求交给过滤器处理。在过滤器中,可以改变请求内容,或者重新设置请求的信息,然后再将请求发送给目标资源。

  • 当目标资源对请求作出响应后,容器同样将响应先转发给过滤器,过滤器可以对响应的内容进行转换,然后再将响应发送到客户端。

在一个Web应用中,可以定义多个过滤器,组成一个过滤器链(FilterChain)。过滤器链中的每个过滤器负责特定的
操作和任务,客户端的请求在这些过滤器之间传递,直到目标资源。



Notes: Filter 不是一个标准的Servlet,不能处理用户请求,也不能对客户端生成响应。主要用于对HttpServletRequest进行预处理,
也可以对HttpServletResponse进行处理,是一个典型的处理链。

Servlet API中提供了一个Filter接口,开发web应用时,如果编写的Java类实现了这个接口,则把这个java类称之为过滤器Filter。

过滤器的英文名称为 Filter, 是 Servlet 技术中最实用的技术。如同它的名字一样,过滤器是处于客户端和服务器资源文件之间的一道过滤网,帮助我们过滤掉一些不符合要求的请求,通常用作 Session 校验,判断用户权限,如果不符合设定条件,则会被拦截到特殊的地址或者基于特殊的响应。

过滤器放在web资源之前,通过Filter技术,可以实现在请求抵达它所应用的web资源(可以是一个Servlet、一个Jsp页面,甚至是一个HTML页面)之前截获进入的请求,并且在它返回到客户之前截获输出请求。

过滤对象

web服务器管理的所有web资源:例如Jsp, Servlet, 静态图片文件或静态 html 文件。

典型应用

  • 对用户请求进行统一认证
  • 对用户发送的数据进行过滤或者替换
  • 对内容进行压缩,以减小通信量

过滤器的使用

自定义过滤器需要实现Filter接口,然后重写它的三个方法

  • init 方法:在容器中创建当前过滤器的时候自动调用
  • destory 方法:在容器中销毁当前过滤器的时候自动调用
  • doFilter 方法:主要的业务代码编写方法,可以多次重复调用

Notes: doFilter(...)实现过滤器的功能。在特定的操作完成之后,可以调用chain.doFilter()方法,
将亲贵传给下一个过滤器(或目标资源),可以直接向客户端返回响应信息,或者利用转发、重定向将请求转发到其他资源。

Filter生命周期

当服务器启动时,web应用加载后,立即创建这个web应用中的所有的过滤器,过滤器创建出来后立即调用init方法执行初始化的操作。

创建出来后一直驻留在内存中为后续的拦截进行服务.每次拦截到请求后都会导致doFilter方法执行。

在服务器关闭或web应用被移除出容器时,随着web应用的销毁过滤器对象销毁.销毁之前调用destory方法执行善后工作。

Filter的创建

Filter的创建和销毁由WEB服务器负责。 web 应用程序启动时,web 服务器将创建Filter 的实例对象,并调用其init方法,完成对象的初始化功能,从而为后续的用户请求作好拦截的准备工作,filter对象只会创建一次,init方法也只会执行一次。通过init方法的参数,可获得代表当前filter配置信息的FilterConfig对象。

Filter的销毁

Web容器调用destroy方法销毁Filter。destroy方法在Filter的生命周期中仅执行一次。在destroy方法中,可以释放过滤器使用的资源。

FilterConfig接口

用户在配置filter时,可以使用为filter配置一些初始化参数,当web容器实例化Filter对象,调用其init方法时,会把封装了filter初始化参数的filterConfig对象传递进来。因此开发人员在编写filter时,通过filterConfig对象的方法,就可获得:

  • String getFilterName():得到filter的名称。
  • String getInitParameter(String name): 返回在部署描述中指定名称的初始化参数的值。如果不存在返回null.
  • Enumeration getInitParameterNames():返回过滤器的所有初始化参数的名字的枚举集合。
  • public ServletContext getServletContext():返回Servlet上下文对象的引用。

过滤器链

A FilterChain is an object provided by the servlet container to the developer giving a view into the invocation chain of a filtered request for a resource. Filters use the FilterChain to invoke the next filter in the chain, or if the calling filter is the last filter in the chain, to invoke the resource at the end of the chain.——《tomcat-5.5-doc》

Web应用允许多个过滤器来过滤页面请求——联想现实生活中的例子是最好理解的啦!比如:为了获得更加干净的水,可能需要多个过滤器来进行过滤。



Servlet定义了过滤器接口Filter和过滤器链接口FilterChain,如下:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package javax.servlet;

import java.io.IOException;

public interface Filter {
    default void init(FilterConfig filterConfig) throws ServletException {
    }

    void doFilter(ServletRequest var1, ServletResponse var2, FilterChain var3) throws IOException, ServletException;

    default void destroy() {
    }
}

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package javax.servlet;

import java.io.IOException;

public interface FilterChain {
    void doFilter(ServletRequest var1, ServletResponse var2) throws IOException, ServletException;
}

所有自定义过滤器都继承自FIlter接口,并实现其doFilter()方法
FiterChain和Filter的接口的doFilter()方法签名不同:

//FilterChain中:
public void doFilter(Target target) {
	if (index == filterList.size()) {
		return;
	}
	//获得下一个过滤器
	Filter f = getNextFilter();
	//执行一个处理器/过滤器的doFilter()方法,然后在该过滤器的doFilter()方法内回调此方法
	f.doFilter(target, this);
}

//Filter中:
public void doFilter(Target target, FilterChain chain) {
	//操作
	...
	
	//回调FilterChain的doFilter方法
	chain.doFilter(target);
}

在前面《Spring拦截器》章节中,有讲到一个对象被多个拦截器拦截处理时,我们称这样的设计模式为责任链模式。同样的道理,一个web请求也可以被多个过滤器来进行处理,这就是FilterChain。

  • 在一个 Web 应用程序中可以注册多个 Filter 程序,每个 Filter 程序都可以对一个或一组 Servlet 程序进行拦截。如果有多个 Filter 程序都可以对某个 Servlet 程序的访问过程进行拦截,当针对该 Servlet 的访问请求到达时,Web 容器将把这多个 Filter 程序组合成一个 Filter 链(也叫过滤器链)。
  • Filter 链中的各个 Filter 的拦截顺序与它们在 web.xml 文件中的映射顺序一致,上一个 Filter.doFilter 方法中调用 FilterChain.doFilter 方法将激活下一个 Filter的doFilter 方法,最后一个 Filter.doFilter 方法中调用的 FilterChain.doFilter 方法将激活目标 Servlet的service 方法。
  • 只要 Filter 链中任意一个 Filter 没有调用 FilterChain.doFilter 方法,则目标 Servlet 的 service 方法都不会被执行。

关于责任链模式在servlet中代码上是如何实现的,有兴趣的同学可以进一步阅读《JAVA与模式》之责任链模式

在Springboot中自定义敏感词过滤器

场景:

在早期的BBS论坛中有很多发帖和回帖的操作,为了保持论坛的和谐,管理员会人工审核和删除一些带有敏感词的帖子。

针对这个场景,如果使用Spring框架,我们可以使用filter进行敏感词的过滤。

新增自定义过滤器

如下自定义过滤器 ReqResFilter 必须实现 javax.servlet.Filter。
然后添加注解 @WebFilter(javax.servlet.annotation.WebFilter),urlPatterns 过滤器要过滤的URL规则配置,filterName 过滤器的名称。

我们新建一个SensitiveWordFilter并实现javax.servlet.Filter

SensitiveWordFilter.java


package com.zhike.filter;
import org.springframework.cglib.proxy.InvocationHandler;
import org.springframework.cglib.proxy.Proxy;
import org.springframework.util.ResourceUtils;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.*;
import java.lang.reflect.Method;
import java.util.ArrayList;

@WebFilter(filterName = "SensitiveWordFilter",urlPatterns = {"/*"})
public class SensitiveWordFilter implements Filter {
    ArrayList<String> list = new ArrayList<String>();

    /**
     * @param
     * @method 初始化过滤词的配置
     */
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        try {
            String line;
            //读取我们的过滤配置文件
            File resource = ResourceUtils.getFile("classpath:static/sensitive.txt");
            BufferedReader sensitivewords = new BufferedReader(new FileReader(resource));
            //把读取到的信息,存放到list中
            while ((line = sensitivewords.readLine()) != null) {
                list.add(line);
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {
        //创建代理对象,增强getParameter
        ServletRequest proxy_req = (ServletRequest) Proxy.newProxyInstance(req.getClass().getClassLoader(), req.getClass().getInterfaces(), new InvocationHandler() {

            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                //判断是不是getParameter方法
                if (method.getName().equals("getParameter")){
                    //获取返回值
                    String value = (String)method.invoke(req, args);
                    if (value != null){
                        for (String s : list){
                            if (value.contains(s)){
                                value = value.replaceAll(s,"***");
                            }
                        }
                    }
                    return value;
                }
                return method.invoke(req,args);
            }
        });
        chain.doFilter(proxy_req, resp);
    }


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


@WebFilter注解

@WebFilter 用于将一个类声明为过滤器,该注解将会在部署时被容器处理,容器将根据具体的属性配置将相应的类部署为过滤器。该注解具有下表给出的一些常用属性 ( 以下所有属性均为可选属性,但是 value、urlPatterns、servletNames 三者必需至少包含一个,且 value 和 urlPatterns 不能共存,如果同时指定,通常忽略 value 的取值 )

@WebFilter 的常用属性
属性名 类型(Filter) 描述(Interceptor)
fileterName String 指定过滤器的Name属性,等价于
value String[] 该属性等价于urlPattems,但两者不应同时使用
urlPattems String[] 指定一组过滤器的URL匹配模式。等价于标签
servlerNames String[] 指定过滤器将应用于哪些servlet。取值是@WebServlet中的name属性的取值或者是web.xml中取值。
dispatchTypes DispatchType 指定过滤器的转发模式。具体取值包括:FORWARD,INCLUDE,REQUEST,ERROR,ASYNC
initParams WebInitParam[] 指定一组过滤器初始化参数 等价于标签
asyncSupported boolean 声明过滤器是否支持异步操作模式,等价于
description String 该过滤器的描述信息, 等价于
displayName String 该过滤器的显示名,通常配合工具使用 等价于

添加 @ServletComponentScan 注解

在启动类上加一个注解 @ServletComponentScan 就可以了,然后启动springboot 访问你的接口就会看到打印过滤器里的内容了。

@SpringBootApplication(scanBasePackages="com.zhike")
@ServletComponentScan(basePackages = "com.zhike")
public class BlogWebappApplication {

	public static void main(String[] args) {
		SpringApplication.run(BlogWebappApplication.class, args);
	}

}

测试用例

通过上面的几个步骤,我们就创建好了一个自定义过滤器。

那如何验证我们的过滤器起作用呢?

正好我们的首页有一个输入框,本来用户输入关键词,通过文章标题来筛选文章。
我们以此为例,假设不消息输入了"坏蛋","笨蛋"这样的敏感词(这里我设置的敏感词库为sensitie.txt)。

sensitie.txt中文本如下:

笨蛋
坏蛋

期望服务端接收到敏感词之后,被替换为“***”。

value = value.replaceAll(s,"***");

我们在DefaultController中Index方法

 @RequestMapping("/index")
    public ModelAndView Index(
            HttpServletRequest request,
            Article article,
            @RequestParam(value = "pageNum",defaultValue = "0") int pageNum,
            @RequestParam(value = "pageSize", defaultValue = "5") int pageSize) {

        String title = request.getParameter("title");
        System.out.println(title);
        .....
}

然后,修改一下/resources/templates/index.html视图

 <div class="row">
                <div class="col-md-12 col-xs-12">
                    <form class="form-inline my-2 my-lg-0" method="get" action="/default/index">
                        <input class="form-control mr-sm-2" type="search" placeholder="Search" aria-label="Search" name="title" th:value="${article.title}">
                        <button class="btn btn-outline-success my-2 my-sm-0" type="submit">搜索</button>
                        <hr/>
                    </form>
                </div>
  </div>         



最后,启动调试,在搜索框中输入"笨蛋",通过查看title变量的值,发现他被替换成了"***"。



由此可见,我们的过滤敏感词功能实现了。

结语

本章节介绍了Spring中过滤器的原理和机制,以及在springboot中如何使用。

源码地址

git地址: https://github.com/zhikecore/superblog

参考

posted @ 2022-02-03 10:40  楠木大叔  阅读(494)  评论(0编辑  收藏  举报