过滤器的功能
tomcat作为一个基于servlet的web服务器,主要职责在于封装请求(ServletRequest)和响应(ServletResponse)对象并分发请求和作出响应
一个java web程序按照tomcat的约定处理请求和响应的具体业务逻辑,本质上,这是一个在应用层基于servlet的程序,即使到了spring框架时代,底层依然如此
一个基于servlet的网络程序不必考虑应用层以及之下的网络通信细节,这些困难复杂繁琐通用而又容易出错的环节,已被封装为各类servlet接口
tomcat在分发请求以及作出响应之前,可以切入一段代码执行切入的业务逻辑,这正是通过其所谓的过滤器完成的
面向servlet编程时代,开发者只需要了解servlet编程中的请求、响应、过滤器等主要对象,就可以专注于自身业务逻辑的开发,完成绝大多数业务的处理
在spring框架编程时代,深度封装了servlet,请求和响应对象高度封装、spring容器内部请求路径映射、切面代码、依赖注入、配置文件以及注解等特性,使得web编程更加简化
在spring2.0即springboot时代,约定大于配置的思想,更多注解的引入,模块化的maven依赖配置等,结合众多插件如lombok的引入,进一步降低了代码量
在spring3.0即springcloud时代,一个集大成的架构层次的分布式的业务群模版的出现,使得架构解决方案成为每个入门开发者都可触及的能力
springfuture时代,只要web程序仍然部署在servlet容器中,框架底层就离不开servlet接口编程
tomcat过滤器本身,就是servlet的一个接口
过滤器的功能就是在分发请求以及作出响应之前切入代码
过滤器的设计原理和配置使用建议
过滤器Filter是定义于tomcat的servlet-api.jar中的一个接口,接口路径为javax.servlet.Filter
实现了该接口的类将被tomcat加载用以在请求处理前后拦截请求,拦截后,用户可对请求的ServletRequest和ServletResponse对象进行各种业务处理
常见的需求如,请求计数、请求地址等信息日志记录、消息加解密和编解码、消息内容检查以及转换和重写、登陆token校验等,都可以在拦截时处理
顾名思义,tomcat过滤器采用了典型的过滤器设计模式,过滤器链FilterChain由tomcat维持,链条是可以支持多个过滤器的
另外,为了分层简化代码,通常一个过滤器只需要完成一项任务,有多种功能需求时,应该配置多个过滤器
当然,根据需要,一个过滤器可以配置为拦截一个或多个资源,这些资源在同一个过滤器中完成同一项过滤任务
除非一个过滤器在web.xml中注册了多次,tomcat只会为每一个过滤器类注册一个对象,即过滤器对象是单例对象
过滤器可能同时处理来自多个用户的网络请求,因此需要注意多线程安全问题
通常,也不需要在过滤器中定义共享的可变变量
当定义了多个过滤器时,顺序通常是很重要的,比如先进行ip白名单过滤,然后再进行token校验,然后再进行请求地址的计数
过滤器由tomcat读取web.xml文件或扫描@WebFilter注解类注册,另外,还可以通过spring的FilterRegistrationBean注册
注解过滤器不能[tomcat8.5及以下,9及以上未验证]指定过滤器顺序
springboot项目约定大于配置,默认是不配置web.xml的,当然,也可以根据需要添加web.xml,web.xml按照上下顺序注册过滤器
FilterRegistrationBean可以指定过滤器顺序
那么应该使用哪种注册方式呢?
web.xml的优点是可以直接修改而不需要重新编译项目代码,缺点是需要配置web.xml文件
FilterRegistrationBean的优点是可以在代码中注册过滤器和指定其顺序,缺点是修改时需要重新编译,当然,如果在一个@Configuration配置类中注册frb,也只是需要重新编译一个java文件而已
能简就简,推荐在前后端分离的项目中使用frb注册过滤器,而在必须或已经存在web.xml的项目中使用web.xml文件注册过滤器
javax.servlet.Filter接口定义了3个方法:init、doFilter、destroy
①void init(FilterConfig filterConfig)
应用程序启动时,servlet容器就会调用init方法,该方法也只会在启动时调用一次
FilterConfig可以从@WebFilter注解中,或者web.xml中读取多个String类型参数进行过滤器对象全局变量的初始化
读取的参数如,免过滤的路径列表,是否启用过滤器等
web.xml配置:
<filter>
<filter-name>filterA</filter-name>
<filter-class>com.liuwei.filter.FilterA</filter-class>
<init-param>
<param-name>freeFilterSet</param-name>
<param-value>/login,/api,/sso</param-value>
</init-param>
<init-param>
<param-name>startFilter</param-name>
<param-value>false</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>filterA</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
类头@WebFilter注解配置:
@WebFilter(filterName = "filterA", urlPatterns = { "/*" }, initParams = {
@WebInitParam(name = "freeFilterSet", value = "/login,/api,/sso"),
@WebInitParam(name = "startFilter", value = "false") })
过滤器类
public class FilterA implements Filter {
// 免过滤的路径集合
private final Set<String> freeFilterSet = new HashSet<String>();
// 是否开启过滤器
private boolean startFilter = true;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("FilterA init:" + TimeUtil.getNow());
String startFilterString = filterConfig.getInitParameter("startFilter");
if (!Boolean.valueOf(startFilterString)) {
startFilter = false;
return;
}
String[] freeFilterList = filterConfig.getInitParameter("freeFilterList").split(",");
for (String freeFilter : freeFilterList) {
freeFilterSet.add(freeFilter);
}
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
//过滤器业务代码
}
@Override
public void destroy() {
System.out.println("FilterA destroy:" + TimeUtil.getNow());
}
}
②void doFilter(ServletRequest request, ServletResponse response,FilterChain chain) throws IOException, ServletException
每次调用过滤器关注的资源前,servlet容器都会先调用Filter对象的doFilter方法
servlet容器会向该方法传入3个必需的参数:ServletRequest、ServletResponse和FilterChain
这3个参数有各自的用处
开发者可以从ServletRequest获取、新增、删除或者修改一个属性,可以根据一些属性值完成token校验、请求地址记录日志、请求转发等任务
可以在ServletResponse中设置一个响应头或者请求重定向(到另一个请求路径)等
当过滤任务执行完成,可以根据需要直接返回响应结果(如校验ip白名单或token失败),或者更多情况下,放行请求到过滤器链的下一个过滤器中
如果需要放行,必须调用FilterChain的doFilter(request, response)方法,反之,如果不调用该方法,就不会放行而是直接返回响应
对于前后端分离的项目,在过滤失败时,返回一个约定格式的json对象,告知前端请求的错误码和详细信息
对于不分离的项目或者接入三方单点登入校验的项目,在过滤失败时,转发或重定向到4xx或5xx等错误视图
特别需要注意的是,过滤器链由一组按照顺序注册的过滤器组成,并且doFilter方法是同步阻塞的
这意味着请求和响应会在链上按顺序且同步地传递,请求时依次从第一个过滤器到最后一个放行进入的过滤器,响应时正相反顺序
注意,在链上的某个过滤器可能因为过滤失败而不再放行到下一个过滤器,因此,此处的最后一个放行进入的过滤器不一定是链上的最后一个过滤器
放行代码即FilterChain.doFilter(request, response)一般是一个过滤器doFilter方法的最后一行代码,这意味着过滤器对返回的响应不作任何处理
但也是可以根据需要后处理ServletResponse对象的,后处理虽然非常少用,但可以据此更清楚地认识整个过滤器链代码执行的顺序
③void destroy()
在过滤器终止服务之前被调用,当应用程序停止时,servlet容器会自动调用
当然,开发者也可以自己调用,比如,根据某个参数判断已经不需要此过滤器了,可以自我调用注销
通常一个应用程序关闭时并不需要在过滤器注销代码中执行任务
Spring框架的一些监听器接口如ServletContextListener、JVM的钩子等都可以执行程序关闭前的任务逻辑
过滤器的注册方式
定义一个过滤器类只需要实现Filter接口即可,但想要被tomcat容器或spring容器识别,还需要进行注册
1.@WebFilter注解
javax.servlet.annotation.WebFilter注解注于Filter实现类头即可
这种方式由于不能指定过滤器顺序,基本不会使用,不详述
2.web.xml配置
范例如下
<filter>
<filter-name>filterA</filter-name>
<filter-class>com.liuwei.filter.FilterA</filter-class>
<init-param>
<param-name>freeFilterSet</param-name>
<param-value>/login,/api,/sso</param-value>
</init-param>
<init-param>
<param-name>startFilter</param-name>
<param-value>false</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>filterA</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
3.FilterRegistrationBean注册
范例如下
在frb中注册的过滤器由spring容器管理,而非servlet容器管理
特别地,一些过滤器中需要依赖注入Spring容器中的bean对象,此时,frb注册是最好的选择
package com.liuwei.configure;
import com.telecom.js.noc.hxtnms.operationplan.filter.AuthFilter;
import com.telecom.js.noc.hxtnms.operationplan.filter.SessionForbiddenFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;
/**
* @author liuwei
* @date 2019-07-26 15:16
* @desc 过滤器注册:解决多过滤器顺序以及依赖注入问题
* 在配置多个过滤器时,Order注解并不能保证过滤器顺序,需要通过FilterRegistrationBean在配置类中注册过滤器
*/
@Configuration
public class FilterRegistrationConfig {
private static final String AUTHOR_KEY = "author";
private static final String AUTHOR_VAL = "liuwei";
/**
* 注册鉴权过滤器:优先级1
*/
@Bean(name = "authFilterRegistration")
@Autowired
public FilterRegistrationBean authFilterRegistration(Filter authFilter) {
FilterRegistrationBean registration = new FilterRegistrationBean(authFilter);
registration.addUrlPatterns("/*");
registration.addInitParameter(AUTHOR_KEY, AUTHOR_VAL);
registration.setName("authFilter");
registration.setOrder(1);
return registration;
}
/**
* 对鉴权过滤器创建Bean对象:因需要进行Spring注入
* 可以使用任何普通Bean的注册方式
* 本例使用配置类注册过滤器为一个普通Bean
* 也可以在过滤器类头添加@Component注解来注册
*/
@Bean(name = "authFilter")
public Filter authFilter() {
return new AuthFilter();
}
/**
* 注册session禁用过滤器:优先级0
*/
@Bean(name = "sessionForbiddenFilterRegistration")
public FilterRegistrationBean sessionForbiddenFilterRegistration() {
FilterRegistrationBean registration = new FilterRegistrationBean(new SessionForbiddenFilter());
registration.addUrlPatterns("/*");
registration.addInitParameter(AUTHOR_KEY, AUTHOR_VAL);
registration.setName("sessionForbiddenFilter");
registration.setOrder(0);
return registration;
}
}