在一个Web应用中,每个Web组件都用于响应特定的客户请求,不过,在这些Web组件响应客户请求的过程中,可能都会完成一些相同的操作。比如都要先检查客户的IP地址是否位于预定义的拒绝IP地址范围内,如果满足这一条件,就直接向客户端返回拒绝响应客户请求的信息,而不会继续执行后续操作。
如果在多个Web组件中编写完成同样操作的程序代码,显然会导致重复编码,从而降低开发效率和软件的可维护性。
为了解决上述问题,过滤器应运而生,它是在JavaServlet 2.3规范中出现的技术。过滤器能够对一部分客户请求先进行预处理操作,然后再把请求转发给相应的Web组件,等到Web组件生成了响应结果后,过滤器还能对响应结果进行检查和修改,然后再把修改后的响应结果发送给客户。各个Web组件的相同操作可以放到同一个过滤器中来完成,这样就能减少重复编码。
过滤器能够对Servlet容器传给Web组件的ServletRequest对象和ServletResponse对象进行检查和修改。过滤器本省并不生成ServletRequest和ServletResponse对象,它只作为Web组件提供如下过滤功能:
· 过滤器能够在Web组件被调用之前检查ServletRequest对象,修改请求头和请求正文的内容,或者对请求进行预处理操作。
过滤器能够在Web组件被调用之后检查ServletResponse对象,修改响应头和响应正文。
过滤器负责过滤的Web组件可以是Servlet、JSP或HTML文件。过滤器的过滤过程如下图:
过滤器既有以下特点:
· 过滤器可以检查ServletRequest和ServletResponse对象,并且利用ServletRequestWrapper和ServletResponseWrapper包装类来修改ServletRequest和ServletResponse对象。
· ·可以在web.xml文件中为过滤器映射特定的URL。当客户请求访问此URL时,Servlet容器就会先触发过滤器工作。
· 过滤器是JavaServlet 2.3规范的一部分,因此所有实现Java Servlet 2.3规范及其以上版本的Servlet容器都支持过滤器。
· 多个过滤器可以被串联在一起,协同为Web组件过滤请求对象和响应对象。
创建过滤器
所有自定义的过滤器都必须实现javax.servlet.Filter接口,这个接口含有以下3个过滤器类必须实现的方法。
· init(FilterConfig config):这是过滤器的初始化方法。在Web应用启动时,Servlet容器先创建包含了过滤器配置信息的FilterConfig对象,然后创建Filter对象,接着调用Filter对象的init(FilterConfig config)方法,在这个方法中可通过config参数来读取web.xml文件中为过滤器配置的初始化参数。
· doFilter(ServletRequest req , ServletResponse res , FilterChainchain):这个方法完成实际的过滤操作。当客户请求访问的URL与为过滤器映射的URL匹配时,Servletrongqi先调用过滤器的doFilter()方法。FilterChain参数用于访问后续过滤器或者Web组件。
· destroy():Servlet容器在销毁过滤器对象前调用该方法,在这个方法中可以释放过滤器占用的资源。
过滤器由Servlet容器创建,在它的生命周期中包含以下阶段。
· 初始化阶段:当Web应用启动时,Servlet容器会加载过滤器类,创建过滤器配置对象(FilterConfig)和过滤器对象,并调用过滤器对象的init(FilterConfig config)方法。
· 运行时阶段:当客户请求访问的URL与为过滤器映射的URL匹配时,Servlet容器将先调用过滤器的doFilter()方法。
· 销毁阶段:当Web应用终止时,Servlet容器将先调用个过滤器对象的destroy()方法,然后销毁过滤器对象。
发布过滤器
在发布过滤器时,必须在web.xml文件中加入<filter>元素和<filter-mapping>元素。<filter>元素用来定义一个过滤器:
<filter> <filter-name>CommonFilter</filter-name> <filter-class>dis.geo.demo.filter.CommonFilter</filter-class> <init-param> <param-name>ipblock</param-name> <param-value>221.45</param-value> </init-param> <init-param> <param-name>blacklist</param-name> <param-value>捣蛋鬼</param-value> </init-param> </filter> <filter-mapping> <filter-name>CommonFilter</filter-name> <filter-pattern>/note</filter-pattern> </filter-mapping>
串联过滤器
多个过滤器可以串联起来协同工作,Servlet容器将根据它们在web.xml中定义的先后顺序,依次调用它们的doFilter()方法。自定义过滤器的doFilter()方法中一般会有这样的代码:chain.doFilter(),chain就是链的意思,也就是说在应用中可以将过滤器串起来,就像是净化水一样一步一步的进行过滤直至满足需求。
示例:黑名单及屏蔽字符串
应用中时常会出现“黑名单”的概念,使用过滤器实现就是检查请求中的特定参数,与指定参数匹配时禁止使用应用中的部分功能。而文字屏蔽更常见,在网上经常会出现某些敏感字符禁止出现的情况,比如马化腾变成***或XXX……
首先需要定义一个过滤器,在过滤器中先判断客户是否在黑名单中,如果在则停止响应或是返回一个拒绝服务的提示。不在黑名单中的客户在进行发布评论等操作时,将过滤掉敏感字符,替换成***,在示例中将屏蔽“夏洛克”、“福尔摩斯”、“华生”这三个字符。正常情况下,应该是有一个屏蔽库,这里使用简单的数组进行模拟。
public class CommonFilter implements Filter{ private String[] strs; public void destroy() { System.out.println("===== commonFilter#destroy ====="); } public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { System.out.println("===== commonFilter#doFilter ====="); HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse res = (HttpServletResponse) response; String loginusername = req.getParameter("loginusername"); if(loginusername != null&&!loginusername.equals("xiaoming")){ //屏蔽敏感字符 String note = req.getParameter("note"); if(note != null && !note.equals("")){ for(String str:strs){ note = note.replaceAll(str, "***"); } } req.setAttribute("note", note); chain.doFilter(req, res); }else{ //拒绝服务 res.setContentType("text/html;charset=utf-8"); PrintWriter out = res.getWriter(); out.println("<b><hr/></b><h1 align='center'>拒绝服务...</h1>"); } } public void init(FilterConfig arg0) throws ServletException { System.out.println("===== commonFilter#init ====="); this.strs = new String[]{"夏洛克" , "福尔摩斯" , "华生"}; } }
在web.xml文件中配置过滤器:
<filter> <filter-name>commonFilter</filter-name> <filter-class>dis.geo.demos.filter.CommonFilter</filter-class> </filter> <filter-mapping> <filter-name>commonFilter</filter-name> <url-pattern>/hello.jsp</url-pattern> </filter-mapping>
login.jsp文件内容:
<body> <br /> <form name="loginForm" method="POST" action="hello.jsp"> <table> <tr> <td><div align="right"><ml:content key="login.user" />:</div></td> <td><input type="text" name="loginusername" /></td> </tr> <tr> <td><div align="right"><ml:content key="login.note" />:</div></td> <td><textarea rows="5" cols="20" name="note"></textarea></td> </tr> <tr> <td></td> <td> <input type="submit" name="submit" value=<ml:content key="login.submit"/>> <input type="reset" name="reset" value=<ml:content key="login.reset"/>> </td> </tr> </table> </form> </body>
hello.jsp:
<body> <b><ml:content key="hello.hi"/> :${param.loginusername}</b> <br/> <p><ml:content key="login.note"/> :<%=request.getAttribute("note") %></p> </body>
启动服务器,按照下面的步骤进行实验:
(1) 访问login.jsp输入用户名“张华”,评论“I‘m 夏洛克·福尔摩斯的得力助手——Dr.华生!!!”点击提交,可以看到访问后页面内容中“夏洛克、福尔摩斯、华生”都被替换成“***”;
(2) 再访问login.jsp输入用户名“xiaoming”,评论“I‘m 夏洛克·福尔摩斯的得力助手——Dr.华生!!!”点击提交,访问后的页面内容为:拒绝访问…
上面的示例中模拟了使用过滤器完成黑名单功能和敏感字符替换功能。修改以下过滤器doFilter方法的代码,在chain.doFilter(req , res);代码行后添加如下代码:
chain.doFilter(req, res);
req.setAttribute("note",
"123456");
res.setStatus(404);
System.out.println("===== After doFilter =====");
打开网页的调试窗口,进行步骤(1),可以看到页面中内容没有发生变化,也就是说在chain.doFilter(req , res)之后对req的修改是无效的,而在网页的调试窗口看到响应的状态为404,也就是说在chain.doFilter(req , res)之后对响应的修改起作用了。而且在控制台看到语句输出===== After doFilter =====。然后把上面res.setStatus(404)修改为res.sendError(404);再进行步骤(1)就可以看到通常情况下的404错误:
上面的步骤中可以看到在chain.doFilter(req , res)之后的代码继续执行,但是此时req对象已经被过滤完毕,传递给了请求目标,再对其进行修改,将不会起作用;但是此时响应还没有被过滤,因此对响应的修改仍然有效。
应用中的多数页面可能是需要用户登录后才能使用的,如果在每个页面中进行判断,将会产生大量的重复代码,此时可以将此任务交给过滤器完成。用户登录后将用户信息保存在session中,如果用户没有登录而直接输入登录后才能使用网页地址,将被拦截到登录页面,可以在应用中增加LoginFilter过滤器
该示例见博客http://blog.csdn.net/goskalrie/article/details/51202741使用Cookie模拟自动登录部分。