概述
在web开发中,想要统一拦截某些请求,实现一些功能。还要能解耦,不与具体业务耦合,这个时候,就可以考虑用filter来做一些事情了。
而filter是怎么实现的呢? 这个肯定与servlet的流转过程有关系了。
从使用的角度来看,
1、一个请求发到web容器,容器通过servlet进行分发,
2、然后容器(①这里有疑问,到底是谁在操控流程的执行?)
检查应用中注册的filter,
根据过滤器的先后顺序,
判断分发方式是否符合,
判断本次请求的url是否匹配filter的映射规则,
然后进入到各filter。
3、到了filter内部,即可拿到本次请求的上下文(②需要了解请求上文下是如何包装起来的?),可以做一些切面性质的工作了。
4、如何跳转下一个filetr,则是调用入参中的filetChain的dofilter 进入到下一个filter或是对应的service服务中去。
至于filter的创建、初始化、销毁,则是一次性的。在spring中是由spring进行管理的,创建时进行init操作。而销毁则是伴随着应用程序的销毁而销毁。(③这点需考证?)
用法
1、注册过滤器
使用spring进行注册的写法
<filter>
<filter-name>encodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<init-param>
<param-name>forceEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>encodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
spring boot中的写法
@Bean
public FilterRegistrationBean registerCheckFilter() {
FilterRegistrationBean registerBean = new FilterRegistrationBean();
//设置过滤哪些路由
this.setDispatcherTypes(registerBean);
//此处省略了设置initParam的代码
//不设置匹配哪些url,则默认匹配/*
registerBean.setOrder(0);
registerBean.setFilter(new CheckEnterCasFilter());
return registerBean;
}
private void setDispatcherTypes(FilterRegistrationBean registerBean) {
EnumSet<DispatcherType> dispatcherTypes = EnumSet.noneOf(DispatcherType.class);
dispatcherTypes.add(DispatcherType.INCLUDE);
dispatcherTypes.add(DispatcherType.REQUEST);
dispatcherTypes.add(DispatcherType.ASYNC);
dispatcherTypes.add(DispatcherType.ERROR);
registerBean.setDispatcherTypes(dispatcherTypes);
}
2、编写过滤器内部代码
public class CheckEnterCasFilter implements Filter {
private CasProperties casProperties;
public CheckEnterCasFilter(CasProperties casProperties) {
this.casProperties = casProperties;
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
//filterConfig可以拿到过滤器配置的initParam的参数,通过它可以给filter传进来一些可配置的选项数据
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String servletPath = httpRequest.getServletPath();
//定制需求代码
LogHelper.monitor(httpRequest.getRequestURI() + "进入调用链");
//进入到过滤器调用链中,如果return则直接返回给调用方了。
chain.doFilter(request, response);
}
@Override
public void destroy() {
}
}
使用场景
1、对接第三方的统一登录认证cas(center authentication server)
- 可以设置过滤器拦截到自己系统中的访问首页的请求,
- 判断我方的session是否有效,无效重定向到第三方cas ,
- 第三方cas 判断session是否过滤,进行登录后再重定向到url中带的我方系统的访问链接,
- 我方系统再次拦截回来的url请求,截获其中包含的关键认证信息,获取人员的相关信息,存到我方的session中
2、防止xss攻击
3、请求中的字符集设置
踩坑记录
1、在做使用场景1时,
需要跳过某些过滤器,就找到了一篇stackoverflow上的博客How to skip a filter in the filter chain in java
,其中提到可以使用request.getRequestDispatcher(servletPath).forward(request, response);
直接访问资源服务。由于不了解filter在过滤时,还会校验request请求的dispatcherType。
导致想跳过AFilter时,始终会重新跳到AFilte中。这样重复进入AFilter的dofilter ,马上栈帧就被撑爆了,导致栈溢出。
解决方法就是上面提到的,我们在设置过滤器时,需要设置这个过滤器能够匹配的请求的分发类型。
简单介绍下,分发类型共有5种。
public enum DispatcherType {
FORWARD,//转发的
INCLUDE,//包含在页面的
REQUEST,//请求的
ASYNC,//异步的
ERROR;//出错的
}
http请求的分发类型(④这里的include、aync、error需深入研究下)
- REQUEST:当用户直接访问页面时,Web容器将会调用过滤器。如果目标资源是通过RequestDispatcher的include()或forward()方法访问时,那么该过滤器就不会被调用。
- INCLUDE:如果目标资源是通过RequestDispatcher的include()方法访问时,那么该过滤器将被调用。除此之外,该过滤器不会被调用。
- FORWARD:如果目标资源是通过RequestDispatcher的forward()方法访问时,那么该过滤器将被调用,除此之外,该过滤器不会被调用。
- ERROR:如果目标资源是通过声明式异常处理机制调用时,那么该过滤器将被调用。除此之外,过滤器不会被调用。
原理
这部分的知识,需要了解tomcat 服务器的运行方式。
目前通过网上一遍博客[3],大致了解流程是,
1.tomcat的连接器(connector)接收到请求后,创建了request、response对象,
2.然后上下文处理器 对请求对象进行wrapper包装,
3.获取到对应的servlet对象(⑤servlet的用法有遗忘?),
4.上下文处理器之后再通过request和servlet 获取过滤器调用链,
5.这时候上下文处理器就会查找当前请求的url与哪些过滤器能够匹配上的,
6.然后一个一个通过过滤器进行过滤,
7.直到过滤器走完了,最后再来调用servlet中的service()
至于service()调完了,剩下的流程应该怎么走呢? 猜想应该还是通过上下文处理器将响应返回给connector,connector再把响应转成2进制数据返回给请求方(浏览器或其他客户端)
后续 读完《深入剖析Tomcat(中文版)》再来详细阐述这个运行机制
源码解析
参考链接
[1]Java Web之过滤器(Filter)
[2]Java三大器之过滤器(Filter)的工作原理和代码演示
[3]tomcat源码分析之filter和servlet