一、Filter概述
Filter 表示过滤器,是 JavaWeb 三大组件(Servlet、Filter、Listener)之一。
过滤器可以把对资源的请求拦截下来,从而实现一些特殊的功能。
如下图所示,浏览器可以访问服务器上的所有的资源(servlet、jsp、html等)
而在访问到这些资源之前可以使过滤器拦截来下,也就是说在访问资源之前会先经过 Filter,如下图
拦截器拦截到后可以做什么功能呢?
过滤器一般完成一些通用的操作。比如每个资源都要写一些代码完成某个功能,我们总不能在每个资源中写这样的代码吧,而此时我们可以将这些代码写在过滤器中,因为请求每一个资源都要经过过滤器。
我们希望用户如果登陆过了就跳转到数据展示的页面;如果没有登陆就跳转到登陆页面让用户进行登陆,要实现这个效果需要在每一个资源中都写上这段逻辑,而像这种通用的操作,我们就可以放在过滤器中进行实现。这个就是权限控制,以后我们还会进行细粒度权限控制。过滤器还可以做 统一编码处理 、 敏感字符处理 等等…
二、Filter快速入门
开发步骤
进行 Filter 开发分成以下三步实现
1、定义类,实现 Filter接口,并重写其所有方法
public class FilterDemo implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { } @Override public void destroy() { } }
2、配置Filter拦截资源的路径
在类上定义 @WebFilter 注解。而注解的 value 属性值 /* 表示拦截所有的资源
@WebFilter("/*") public class FilterDemo implements Filter {}
3、在doFilter方法中输出一句话,并放行
@Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { System.out.println("filter被执行了..."); filterChain.doFilter(servletRequest,servletResponse); }
上述代码中的 chain.doFilter(request,response); 就是放行,也就是让其访问本该访问的资源。
代码演示
我们在springboot项目中创建一个filter目录,该目录下新建FilterDemo文件
@WebFilter("/*") @Configuration public class FilterDemo implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { System.out.println("filter被执行了..."); filterChain.doFilter(servletRequest,servletResponse); } @Override public void destroy() { } }
注意:要添加@Configuration注解,过滤器才会生效。
当我们请求接口:http://localhost:8090/api/user/list,控制台就会打印
filter被执行了...
上述效果说明 FilterDemo 这个过滤器的 doFilter() 方法执行了
三、Filter执行流程
如上图是使用过滤器的流程,我们通过以下问题来研究过滤器的执行流程:
问:放行后访问对应资源,资源访问完成后,还会回到Filter中吗?
答:从上图就可以看出肯定 会 回到Filter中
问:如果回到Filter中,是重头执行还是执行放行后的逻辑呢?
答:如果是重头执行的话,就意味着 放行前逻辑 会被执行两次,肯定不会这样设计了;所以访问完资源后,会回到 放行后逻辑 ,执行该部分代码。
通过上述的说明,我们就可以总结Filter的执行流程如下:
接下来我们通过代码验证一下,在 doFilter() 方法前后都加上输出语句,如下
@WebFilter("/*") @Configuration public class FilterDemo implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { // 放行前 System.out.println("1.FileterDemo...."); // 放行 filterChain.doFilter(servletRequest,servletResponse); // 放行后 System.out.println("3.FileterDemo...."); } @Override public void destroy() { } }
接口如下:
@RestController @RequestMapping("/api/user") @Slf4j public class UserController { @Resource private UserDao userDao; @GetMapping("/list") public Result getList(){ List<User> userList = userDao.selectAll(); System.out.println("2.FilterDemo..."); return Result.operating("查询用户信息成功", true, ResultCode.SUCCESS,userList); } }
执行访问该资源打印的顺序是按照我们标记的标号进行打印的话,说明我们上边总结出来的流程是没有问题的。
浏览器访问http://localhost:8090/api/user/list,控制台就会打印
1.FileterDemo.... 2.FilterDemo... 3.FileterDemo....
以后我们可以将对请求进行处理的代码放在放行之前进行处理,而如果请求完资源后还要对响应的数据进行处理时可以在放行后进行逻辑处理。
四、Filter拦截路径配置
拦截路径表示 Filter 会对请求的哪些资源进行拦截,使用 @WebFilter 注解进行配置。如: @WebFilter("拦截路径")
拦截路径有如下四种配置方式:
(1)、拦截具体的资源:/index.jsp:只有访问index.jsp时才会被拦截
(2)、目录拦截:/user/*:访问/user下的所有资源,都会被拦截
(3)、后缀名拦截:*.jsp:访问后缀名为jsp的资源,都会被拦截
(4)、拦截所有:/*:访问所有资源,都会被拦截
通过上面拦截路径的学习,大家会发现拦截路径的配置方式和 Servlet 的请求资源路径配置方式一样,但是表示的含义不同。
五、过滤器链
过滤器链是指在一个Web应用,可以配置多个过滤器,这多个过滤器称为过滤器链。
如下图就是一个过滤器链,我们学习过滤器链主要是学习过滤器链执行的流程
上图中的过滤器链执行是按照以下流程执行:
1. 执行 Filter1 的放行前逻辑代码
2. 执行 Filter1 的放行代码
3. 执行 Filter2 的放行前逻辑代码
4. 执行 Filter2 的放行代码
5. 访问到资源
6. 执行 Filter2 的放行后逻辑代码
7. 执行 Filter1 的放行后逻辑代码
以上流程串起来就像一条链子,故称之为过滤器链。
代码演示
我们再编写一个过滤器FilterDemo2
@WebFilter("/*") @Configuration public class FilterDemo2 implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { // 放行前 System.out.println("1.1.FileterDemo...."); // 放行 filterChain.doFilter(servletRequest,servletResponse); // 放行后 System.out.println("3.1.FileterDemo...."); } @Override public void destroy() { } }
浏览器访问http://localhost:8090/api/user/list,控制台就会打印
1.FileterDemo.... 1.1.FileterDemo.... 2.FileterDemo.... 3.1.FileterDemo.... 3.FileterDemo....
从结果可以看到确实是按照我们之前说的执行流程进行执行的。
问题
上面代码中为什么是先执行 FilterDemo ,后执行 FilterDemo2 呢?
我们现在使用的是注解配置Filter,而这种配置方式的优先级是按照过滤器类名(字符串)的自然排序。
比如有如下两个名称的过滤器 : BFilterDemo 和 AFilterDemo 。那一定是 AFilterDemo 过滤器先执行。
六、编写配置类注册Filter
注意:上面我们编写的过滤器都加了@Configuration注解,如果两个过滤器都不加的话,是无法拦截请求的。此时控制台打印如下:
2.FileterDemo....
此时我们可以编写配置类来注册Filter
@Configuration public class FilterConfig { @Bean public FilterRegistrationBean RegistTest1(){ //通过FilterRegistrationBean实例设置优先级可以生效 //通过@WebFilter无效 FilterRegistrationBean registration = new FilterRegistrationBean(); registration.setFilter(new FilterDemo());//注册自定义过滤器 registration.setName("flilter1");//过滤器名称 registration.addUrlPatterns("/*");//过滤所有路径 registration.setOrder(1);//优先级,最顶级,值越小越优先 return registration; } @Bean public FilterRegistrationBean RegistTest2(){ //通过FilterRegistrationBean实例设置优先级可以生效 //通过@WebFilter无效 FilterRegistrationBean registration = new FilterRegistrationBean(); registration.setFilter(new FilterDemo2());//注册自定义过滤器 registration.setName("flilter2");//过滤器名称 registration.addUrlPatterns("/*");//过滤所有路径 registration.setOrder(6);//优先级,最顶级,值越小越优先 return registration; } }
浏览器访问http://localhost:8090/api/user/list,控制台就会打印
1.FileterDemo.... 1.1.FileterDemo.... 2.FileterDemo.... 3.1.FileterDemo.... 3.FileterDemo....
七、后台使用过滤器来解决跨站点请求伪造Csrf
如果referer不包含请求的域名,则为csrf攻击,拒绝访问。
@Slf4j @Configuration @WebFilter(filterName = "CsrfFilter", urlPatterns = "/*") public class CsrfFilter implements Filter { /** * 过滤器配置对象 */ FilterConfig filterConfig = null; /** * 是否启用 */ private boolean enable = true; /** * 忽略的URL */ @Value("${security.csrf.excludes}") private String excludes; @Override public void init(FilterConfig filterConfig) { this.filterConfig = filterConfig; } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) servletRequest; // 不启用或者已忽略的URL不拦截 if (!enable || isExcludeUrl(request.getServletPath())) { filterChain.doFilter(servletRequest, servletResponse); return; } String referer = request.getHeader("Referer"); String serverName = request.getServerName(); if (serverName.contains("localhost") || serverName.contains("127.0.0.1")) { filterChain.doFilter(servletRequest, servletResponse); } else { // 判断是否存在外链请求本站 if (null != referer && referer.indexOf(serverName) < 0) { log.error("系统不支持当前域名的访问=> serverName:{} => referer:{}", serverName, referer); Result error = Result.error("系统不支持当前域名的访问,referer错误!", ResultCode.ILLEGAL_ARGUMENT); servletResponse.setContentType("application/json; charset=utf-8"); servletResponse.getWriter().write(JSON.toJSONString(error)); servletResponse.getWriter().flush(); } else { filterChain.doFilter(servletRequest, servletResponse); } } } @Override public void destroy() { this.filterConfig = null; } /** * 判断是否为忽略的URL * * @param url * URL路径 * @return true-忽略,false-过滤 */ private boolean isExcludeUrl(String url) { if (excludes == null || excludes.isEmpty()) { return false; } List<String> urls = Arrays.asList(excludes.split(",")); return urls.stream().map(pattern -> Pattern.compile("^" + pattern)).map(p -> p.matcher(url)) .anyMatch(Matcher::find); } }
八、后台使用过滤器来解决跨域和XSS攻击
HttpServletRequestWrapper 采用装饰者模式对HttpServletRequest进行包装,我们可以通过子类继承HttpServletRequestWrapper 类去重写getParameterValues,getParameter等方法,实际还是调用HttpServletRequest的相对应方法,但是可以对方法的结果进行改装。
解决跨域的过滤器(注意:要对ServletRequest进行包装)
@WebFilter(filterName = "jsonFilter", urlPatterns = "/*") public class JsoupFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletResponse response = (HttpServletResponse) servletResponse; response.setHeader("Access-Control-Allow-Origin", "*"); response.setHeader("Access-Control-Allow-Credentials", "true"); //是否支持cookie跨域 response.setHeader("Access-Control-Allow-Headers", "content-type,x-requested-with,Authorization,auth"); filterChain.doFilter(new XssHttpServletRequestWrapper((HttpServletRequest) servletRequest), servletResponse); } @Override public void destroy() { } }
解决XSS(跨站脚本攻击)攻击的过滤器(注意:要对ServletRequest进行包装)
XSS攻击解决办法:在表单提交或者url参数传递前,对需要的参数进行过滤。
检查用户输入的内容中是否有非法内容:如<>(尖括号)、”(引号)、 ‘(单引号)、%(百分比符号)、;(分号)、()(括号)、&(& 符号)、+(加号)等,严格控制输出。
public class XssFilter implements Filter { FilterConfig filterConfig = null; @Override public void init(FilterConfig filterConfig) throws ServletException { System.out.println("init"); this.filterConfig = filterConfig; } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { chain.doFilter(new XssHttpServletRequestWrapper((HttpServletRequest)request), response); } @Override public void destroy() { this.filterConfig = null; } }
编写HttpServletRequestWrapper的子类
public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper { public XssHttpServletRequestWrapper(HttpServletRequest servletRequest) { super(servletRequest); } public String[] getParameterValues(String parameter) { String[] values = super.getParameterValues(parameter); if (values==null) { return null; } int count = values.length; String[] encodedValues = new String[count]; for (int i = 0; i < count; i++) { encodedValues[i] = cleanXSS(values[i]); } return encodedValues; } public String getParameter(String parameter) { String value = super.getParameter(parameter); if (value == null) { return null; } return cleanXSS(value); } public String getHeader(String name) { String value = super.getHeader(name); if (value == null) return null; return cleanXSS(value); } private String cleanXSS(String value) { //You'll need to remove the spaces from the html entities below value = value.replaceAll("<", "& lt;").replaceAll(">", "& gt;"); value = value.replaceAll("\\(", "& #40;").replaceAll("\\)", "& #41;"); value = value.replaceAll("'", "& #39;"); value = value.replaceAll("eval\\((.*)\\)", ""); value = value.replaceAll("[\\\"\\\'][\\s]*javascript:(.*)[\\\"\\\']", "\"\""); value = value.replaceAll("script", ""); return value; } }
这样一来,在servlet中调用包装器的getParameterValuess方法来获取参数,就已经完成了对参数的过滤过程,我们就不需要在每次获取参数时来进行过滤了。
编写配置类注册filter
@Configuration public class FilterConfig { @SuppressWarnings({ "rawtypes", "unchecked" }) @Bean @Order(Integer.MAX_VALUE-1) public FilterRegistrationBean xssFilterRegistration() { FilterRegistrationBean registration = new FilterRegistrationBean(); registration.setDispatcherTypes(DispatcherType.REQUEST); registration.setFilter(new XssFilter()); registration.addUrlPatterns("/*"); registration.setName("xssFilter"); registration.setOrder(Integer.MAX_VALUE-1); Map<String, String> initParameters = Maps.newHashMap(); initParameters.put("excludes", "/system/notice/*"); initParameters.put("excludes", "/module/messagePush/*"); registration.setInitParameters(initParameters); return registration; } @Bean @Order(Integer.MAX_VALUE) public FilterRegistrationBean someFilterRegistration() { FilterRegistrationBean registration = new FilterRegistrationBean(); registration.setDispatcherTypes(DispatcherType.REQUEST); registration.setFilter(new JsoupFilter()); registration.addUrlPatterns("/*"); registration.setName("jsonFilter"); registration.setOrder(Integer.MAX_VALUE); Map<String, String> initParameters = Maps.newHashMap(); registration.setInitParameters(initParameters); return registration; } }