美文网首页web安全
JavaWeb--Servlet过滤器Filter和Spring

JavaWeb--Servlet过滤器Filter和Spring

作者: JackFrost_fuzhu | 来源:发表于2017-05-04 09:34 被阅读331次

    拦截一些请求进行处理,比如通过它来进行权限验证,或者是来判断用户是否登陆,日志记录,编码,或者限制时间点访问等等,是非常有必要的。所以就有了此篇文章啦。

    文章结构:(1)Servlet过滤器Filter;(2)SpringMVC的HandlerInterceptor;(3)对比认知。

    一、Servlet过滤器Filter:

    此部分是从赵四大佬那里学来的,并补充自己的认知

    (1)概念:

    能够对Servlet容器的请求和响应对象进行检查和修改。

    Servlet过滤器本身并不产生请求和响应对象,它只能提供过滤作用。Servlet过期能够在Servlet被调用之前检查Request对象,修改Request Header和Request内容;在Servlet被调用之后检查Response对象,修改Response Header和Response内容。

    Servlet过期负责过滤的Web组件可以是Servlet、JSP或者HTML文件。

    总之就是:实现用户在访问某个目标资源之前,对访问的请求和响应进行拦截。

    (2)特点:

    A.Servlet过滤器可以检查和修改ServletRequest和ServletResponse对象

    B.Servlet过滤器可以被指定和特定的URL关联,只有当客户请求访问该URL时,才会触发过滤器

    C.Servlet过滤器可以被串联在一起,形成管道效应,协同修改请求和响应对象

    (3)作用:

    A.查询请求并作出相应的行动。

    B.阻塞请求-响应对,使其不能进一步传递。

    C.修改请求的头部和数据。用户可以提供自定义的请求。

    D.修改响应的头部和数据。用户可以通过提供定制的响应版本实现。

    E.与外部资源进行交互。

    (3)适用场合:

    A.认证过滤

    B.登录和审核过滤

    C.图像转换过滤

    D.数据压缩过滤

    E.加密过滤

    F.令牌过滤

    G.资源访问触发事件过滤

    H.XSL/T过滤

    I.Mime-type过滤

    (4)认知Filter接口源码:

    public interface Filter {
            //Servlet过滤器的初始化方法,Servlet容器创建Servlet过滤器实例后将调用这个方法。在这个方法中可以读取web.xml文件中Servlet过滤器的初始化参数
        public void init(FilterConfig filterConfig) throws ServletException;
        //完成实际的过滤操作,当客户请求访问于过滤器关联的URL时,Servlet容器将先调用过滤器的doFilter方法。FilterChain参数用于访问后续过滤器
        public void doFilter(ServletRequest request, ServletResponse response,
                FilterChain chain) throws IOException, ServletException;
             //Servlet容器在销毁过滤器实例前调用该方法,这个方法中可以释放Servlet过滤器占用的资源
        public void destroy();
    }
    

    (5)认知Filter为何可以拦截请求:(本博主找不到具体的代码调用,只能找到相关的几个接口估测)

    FilterChain

    public interface FilterChain {
    //此方法是由Servlet容器提供给开发者的,用于对资源请求过滤链的依次调用,通过FilterChain调用过滤链中的下一个过滤  器,如果是最后一个过滤器,则下一个就调用目标资源。
        public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException;
    }
    

    FilterConfig。FilterConfig接口检索过滤器名、初始化参数以及活动的Servlet上下文。

    public interface FilterConfig {
     //返回web.xml部署文件中定义的该过滤器的名称
        String getFilterName();
     //返回调用者所处的servlet上下文
        ServletContext getServletContext();
    //返回过滤器初始化参数值的字符串形式,当参数不存在时,返回nul1.name是初始化参数名
        String getInitParameter(String var1);
    //以Enumeration形式返回过滤器所有初始化参数值,如果没有初始化参数,返回为空
        Enumeration<String> getInitParameterNames();
    }
    

    博主猜测是:web服务器的底层源码机制的模板方法模式,默认调用doFilter时,先去执行filterClain的doFilter。从而进行拦截

    (6)Servlet过滤器对响应的过滤过程:

    A.过滤器截获客户端的请求

    B.重新封装ServletResponse,在封装后的ServletResponse中提供用户自定义的输出流

    C.将请求向后续传递

    D.Web组件产生响应

    E.从封装后的ServletResponse中获取用户自定义的输出流

    F.将响应内容通过用户自定义的输出流写入到缓冲流中

    G.在缓冲流中修改响应的内容后清空缓冲流,输出响应内容

    (7)实例:(取自上文介绍的赵四大佬那里)

    1. 注册:在web.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" id="WebApp_ID" version="3.0">
      <display-name>testFilter</display-name>
      
      <!-- 请求url日志记录过滤器 -->    
        <filter>    
            <filter-name>logfilter</filter-name>    
            <filter-class>com.fuzhu.LogFilter</filter-class>    
        </filter>    
        <filter-mapping>    
            <filter-name>logfilter</filter-name>    
            <url-pattern>/*</url-pattern>    
        </filter-mapping>  
          
    <!-- 编码过滤器 -->    
        <filter>    
            <filter-name>setCharacterEncoding</filter-name>    
            <filter-class>com.fuzhu.EncodingFilter</filter-class>    
            <init-param>    
                <param-name>encoding</param-name>    
                <param-value>utf-8</param-value>    
            </init-param>    
        </filter>    
        <filter-mapping>    
            <filter-name>setCharacterEncoding</filter-name>    
            <url-pattern>/*</url-pattern>    
        </filter-mapping>    
    </web-app>
    

    既然注册了,那实现呢?

    package com.fuzhu;
    
    import java.io.IOException;
    import java.util.Enumeration;
    import java.util.HashMap;
    
    import javax.servlet.Filter;
    import javax.servlet.FilterChain;
    import javax.servlet.FilterConfig;
    import javax.servlet.ServletException;
    import javax.servlet.ServletRequest;
    import javax.servlet.ServletResponse;
    
    public class EncodingFilter implements Filter {    
        private String encoding;    
        private HashMap<String,String> params = new HashMap<String,String>();    
        // 项目结束时就已经进行销毁    
        public void destroy() {    
            System.out.println("end do the encoding filter!");    
            params=null;    
            encoding=null;    
        }    
        public void doFilter(ServletRequest req, ServletResponse resp,FilterChain chain) throws IOException, ServletException {    
            System.out.println("before encoding " + encoding + " filter!");    
            req.setCharacterEncoding(encoding);    
            chain.doFilter(req, resp);          
            System.out.println("after encoding " + encoding + " filter!");    
            System.err.println("----------------------------------------");    
        }    
         
        // 项目启动时就已经进行读取    
        public void init(FilterConfig config) throws ServletException {    
            System.out.println("begin do the encoding filter!");    
            encoding = config.getInitParameter("encoding");    
            for (Enumeration<?> e = config.getInitParameterNames(); e.hasMoreElements();) {    
                String name = (String) e.nextElement();    
                String value = config.getInitParameter(name);    
                params.put(name, value);    
            }    
        }    
     }    
    
    
    package com.fuzhu;
    
    import java.io.IOException;
    
    import javax.servlet.Filter;
    import javax.servlet.FilterChain;
    import javax.servlet.FilterConfig;
    import javax.servlet.ServletException;
    import javax.servlet.ServletRequest;
    import javax.servlet.ServletResponse;
    import javax.servlet.http.HttpServletRequest;
    
    public class LogFilter implements Filter {    
        
      public FilterConfig config;    
       
      public void destroy() {    
          this.config = null;    
          System.out.println("end do the logging filter!");  
      }    
       
      public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {    
          System.out.println("before the log filter!");    
          // 将请求转换成HttpServletRequest 请求    
          HttpServletRequest hreq = (HttpServletRequest) req;    
          // 记录日志    
          System.out.println("Log Filter已经截获到用户的请求的地址:"+hreq.getServletPath() );    
          try {    
              // Filter 只是链式处理,请求依然转发到目的地址。    
              chain.doFilter(req, res);    
          } catch (Exception e) {    
              e.printStackTrace();    
          }    
          System.out.println("after the log filter!");    
      }    
       
      public void init(FilterConfig config) throws ServletException {    
          System.out.println("begin do the log filter!");    
          this.config = config;    
      }    
       
    }    
    
    

    接下来就是测试啦:

    package com.fuzhu;
    
    import java.io.IOException;
    
    import javax.servlet.ServletException;
    import javax.servlet.annotation.WebServlet;
    import javax.servlet.http.HttpServlet;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    @WebServlet("/FilterServlet")
    public class FilterServlet extends HttpServlet {  
      
        private static final long serialVersionUID = 1L;  
      
        public void doGet(HttpServletRequest request, HttpServletResponse response)  
                throws ServletException, IOException {  
            response.setDateHeader("expires", -1);  
        }  
      
        public void doPost(HttpServletRequest request, HttpServletResponse response)  
                throws ServletException, IOException {  
        }  
      
    }  
    

    再着就是发布啦。注意下,我们先注册日志过滤器,然后注册编码器

    这里写图片描述

    (8)我们来看下注意事项:

    A.由于Filter、FilterConfig、FilterChain都是位于javax.servlet包下,并非HTTP包所特有的,所以其中所用到的请求、响应对象ServletRequest、ServletResponse在使用前都必须先转换成HttpServletRequest、HttpServletResponse再进行下一步操作。

    B.在web.xml中配置Servlet和Servlet过滤器,应该先声明过滤器元素,再声明Servlet元素

    C.如果要在Servlet中观察过滤器生成的日志,应该确保在server.xml的localhost对应的<host>元素中配置如下<logger>元素:

    <Logger className = “org.apache.catalina.logger.FileLogger”

    directory = “logs”prefix = “localhost_log.”suffix=”.txt”

    timestamp = “true”/>


    二、SpringMVC的HandlerInterceptor(给出一个登录功能的拦截Demo)

    (1)springMVC拦截器的实现一般有两种方式:

    第一种方式是要定义的Interceptor类要实现了Spring的HandlerInterceptor 接口

    第二种方式是继承实现了HandlerInterceptor接口的类,比如Spring已经提供的实现了HandlerInterceptor接口的抽象类HandlerInterceptorAdapter

    (2)概念及作用:

    概念:Spring MVC允许你通过处理拦截拦截web请求,进行前置处理和后置处理。处理拦截是在Spring的web应用程序上下文中配置的,因此它们可以利用各种容器特性,并引用容器中声明的任何Bean。处理拦截是针对特殊的处理程序映射进行注册的,因此它只拦截通过这些处理程序映射的请求。

    主要作用是拦截用户的请求并进行相应的处理,其他的作用比如通过它来进行权限验证,或者是来判断用户是否登陆,日志记录,或者限制时间点访问。

    (3)认识HandlerInterceptor接口:

    public interface HandlerInterceptor {
    //该方法将在请求处理之前进行调用。该方法将在请求处理之前进行调用,只有该方法返回true,才会继续执行后续的Interceptor和Controller
        boolean preHandle(HttpServletRequest var1, HttpServletResponse var2, Object var3) throws Exception;
    //在当前请求进行处理之后,也就是Controller 方法调用之后执行
        void postHandle(HttpServletRequest var1, HttpServletResponse var2, Object var3, ModelAndView var4) throws Exception;
    //该方法将在整个请求结束之后,也就是在DispatcherServlet 渲染了对应的视图之后执行。
        void afterCompletion(HttpServletRequest var1, HttpServletResponse var2, Object var3, Exception var4) throws Exception;
    }
    
    

    preHandle:

    SpringMVC 中的Interceptor 是链式的调用的,在一个应用中或者说是在一个请求中可以同时存在多个Interceptor 。每个Interceptor 的调用会依据它的声明顺序依次执行,而且最先执行的都是Interceptor 中的preHandle 方法,所以可以在这个方法中进行一些前置初始化操作或者是对当前请求的一个预处理,也可以在这个方法中进行一些判断来决定请求是否要继续进行下去。该方法的返回值是布尔值Boolean 类型的,当它返回为false 时,表示请求结束,后续的Interceptor 和Controller 都不会再执行;当返回值为true 时就会继续调用下一个Interceptor 的preHandle 方法,如果已经是最后一个Interceptor 的时候就会是调用当前请求的Controller 方法。

    postHandle:

    只能是在当前所属的Interceptor 的preHandle 方法的返回值为true 时才能被调用。Controller 方法调用之后执行,但是它会在DispatcherServlet 进行视图返回渲染之前被调用,所以我们可以在这个方法中对Controller 处理之后的ModelAndView 对象进行操作。postHandle 方法被调用的方向跟preHandle 是相反的,也就是说先声明的Interceptor 的postHandle 方法反而会后执行。

    afterCompletion:

    该方法也是需要当前对应的Interceptor 的preHandle 方法的返回值为true 时才会执行。顾名思义,该方法将在整个请求结束之后,也就是在DispatcherServlet 渲染了对应的视图之后执行。这个方法的主要作用是用于进行资源清理工作的。 我们的系统日志的拦截在这个方法中,可以记录日志的相关的参数,检测方法的执行。

    第一个和第二个方法分别是在处理程序处理请求之前和之后被调用的。第二个方法还允许访问返回的ModelAndView对象,因此可以在它里面操作模型属性。最后一个方法是在所有请求处理完成之后被调用的(如视图呈现之后).

    (4)先来个Helloworld:

    在springmvc配置文件那里配置拦截器,拦截的请求。

    可以看到我一会实现的一个功能啦,登录的认证。
    <!-- 配置拦截器 -->
        <mvc:interceptors>
            <!-- 配置登陆拦截器 -->
            <mvc:interceptor>
                <!--拦截后台页面的请求-->
                <!--<mvc:mapping path="/backend/**"/>-->
                <mvc:mapping path="/test/testMethod"/>
                <!--不拦截登录页和登录的请求-->
                <!--<mvc:exclude-mapping path="/backend/loginPage"/>-->
                <!--<mvc:exclude-mapping path="/backend/login"/>-->
                <bean class="com.fuzhu.Interceptor.Myinterceptor"></bean>
            </mvc:interceptor>
        </mvc:interceptors>
    

    请求的实现:

    @RestController
    @RequestMapping("/test")
    public class TestController {
     @RequestMapping(value = "/testMethod",produces="text/html;charset=UTF-8", method = {RequestMethod.GET,RequestMethod.GET})
        public String test() {
            Score score = new Score();
    //        score.setChangeType("玩游戏");
    //        score.setScore(10);
    //        scoreService.insertScore(score);
            return JSON.toJSONString(score);
        }
    }
    
    这里写图片描述 这里写图片描述

    (5)实现后台管理页面的登录拦截(其实就是面向前端的拦截)

    作用是:就是在后台管理者登录之前,都必须经过登录才可进入管理界面,其他侵入性的接口也不可访问。

    (一)注册我们的拦截器先:

     <!-- 配置拦截器 -->
        <mvc:interceptors>
            <!-- 配置登陆拦截器 -->
            <mvc:interceptor>
                <!--拦截后台页面的请求-->
                <mvc:mapping path="/backend/**"/>
                <!--<mvc:mapping path="/test/testMethod"/>-->
                <!--不拦截登录页和登录的请求-->
                <mvc:exclude-mapping path="/backend/loginPage"/>
                <mvc:exclude-mapping path="/backend/login"/>
                <!--<bean class="com.fuzhu.Interceptor.Myinterceptor"></bean>-->
                <bean class="com.fuzhu.Interceptor.LoginInterceptor"></bean>
            </mvc:interceptor>
        </mvc:interceptors>
    

    (二)实现拦截器:针对后台管理系统的需要,不能让别人随便用url直接访问到别的服务器应用请求,必须先登录。所以我们做一个登录认证的拦截器。而且需要认证一系列的cookie,session

    public class LoginInterceptor implements HandlerInterceptor {
    
        @Override
        public void afterCompletion(HttpServletRequest request,
                                    HttpServletResponse response, Object obj, Exception err)
                throws Exception {
        }
    
        @Override
        public void postHandle(HttpServletRequest request, HttpServletResponse response,
                               Object obj, ModelAndView mav) throws Exception {
    
        }
    
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
                                 Object obj) throws Exception {
              //拿到cookie
              //也就是获取session里的登录状态值
            String cookie= CookieUtil.getByName(request,"isLogin");
            if (cookie!=null){
            //session解密
                Map<String,Object> map= AuthUtil.decodeSession(cookie);
                String loginStatus= (String) map.get("isLogin");
                Long timestamp= (Long) map.get("timestamp");
                if (loginStatus!=null&&timestamp!=null&&new Date().getTime()-timestamp<1000*60*60*24*10){
                    return true;
                }
            }
            //没有找到登录状态则重定向到登录页,返回false,不执行原来controller的方法
            response.sendRedirect("/backend/loginPage");
            return false;
        }
    }
    

    (三)为了充分去了解Session和cookie机制,本博主再做了两个工具类,一个是session的工具类,一个是cookie的工具类,还有另外一篇解析博客(附带面向客户端的Demo)--也是一个目前最流行的认证方式--Token认证

    WEB后台--基于Token的WEB后台登录认证机制(并讲解其他认证机制以及cookie和session机制)

    //session工具类
    public class AuthUtil {
    //这个类方法是面向手机客户端的,从而实现的Token机制。实现请见上述文章:
        private static Map<String, Object> getClientLoginInfo(HttpServletRequest request) throws Exception {
            Map<String, Object> r = new HashMap<>();
            String sessionId = request.getHeader("sessionId");
            if (sessionId != null) {
                r = decodeSession(sessionId);
                return r;
            }
            throw new Exception("session解析错误");
        }
    //根据token拿去用户id
        public static Long getUserId(HttpServletRequest request) throws Exception {
            return  Long.valueOf((Integer)getClientLoginInfo(request).get("userId"));
    
        }
    
        /**
         * session解密
         */
        public static Map<String, Object> decodeSession(String sessionId) {
            try {
                return verifyJavaWebToken(sessionId);
            } catch (Exception e) {
                System.err.println("");
                return null;
            }
        }
    }
    
    

    cookie工具类:

    public class CookieUtil {
    
        public static final int TIME = 60 * 60 * 24 * 10;  //10天存活时间
    //添加cookie
        public static void addCookie(HttpServletResponse response,
                                     String cookieName, String value) {
            Cookie cookie = new Cookie(cookieName, value);
            cookie.setPath("/");
            cookie.setMaxAge(TIME);
            response.addCookie(cookie);
        }
    //删除cookie
        public static void deleteCookie(HttpServletResponse response,
                                        String cookieName) {
            Cookie cookie = new Cookie(cookieName, null);
            cookie.setPath("/");
            cookie.setMaxAge(0);
            response.addCookie(cookie);
        }
    //获取用户的cookie名字
        public static String getByName(HttpServletRequest request, String cookieName) {
            String value = null;
            Cookie[] cookies = request.getCookies();
            if (cookies != null) {
                for (Cookie cookie : cookies) {
                    if (cookieName.equals(cookie.getName())) {
                        value = cookie.getValue();
                    }
                }
            }
            return value;
        }
    
    }
    

    (四)怎么去使用这个机制以及拦截器呢??

    @Controller
    @RequestMapping("/backend")
    public class BackstageController {
    //首先是登录页面
        @RequestMapping(value = "/loginPage", method = {RequestMethod.GET})
        public String loginPage(HttpServletRequest request, String account, String password) {
            return "login";
        }
    //登录的接口逻辑
        @RequestMapping(value = "/login", method = {RequestMethod.POST})
        public String login(HttpServletRequest request, HttpServletResponse response, RedirectAttributes model, String account, String password) {
        //后台管理者的账号密码
            if ("fuzhu".equals(account) && "fuzhucheng".equals(password)) {
                Map<String, Object> loginInfo = new HashMap<>();
                loginInfo.put("isLogin", "yes!");
                loginInfo.put("timestamp", new Date());
                String sessionId = JavaWebToken.createJavaWebToken(loginInfo);//token机制,详情请看上文所说的文章
                CookieUtil.addCookie(response,"isLogin",sessionId);//加cookie
                return "redirect:loginSuccess";//重定向
            } else {
                model.addFlashAttribute("error", "密码错误");
                return "redirect:loginPage";
            }
        }
        @RequestMapping(value = "/loginSuccess", method = {RequestMethod.GET})
        public String accusationPage(HttpServletRequest request) {
            return "success";
        }
        //主动登出的时候使用
        @RequestMapping(value = "/logOut", method = {RequestMethod.GET})
        public String loginOut(HttpServletRequest request, HttpServletResponse response) {
            CookieUtil.deleteCookie(response,"isLogin");
            return "redirect:loginPage";
        }
    }
    

    (五)演示:

    这里写图片描述
    登录成功
    这里写图片描述
    如果我们不登录,直接访问登录成功的url。结果如下:默认返回登录页面(因为我们拦截重定向了)
    这里写图片描述

    三、对比认知:阅读上文所述的接口并参考此文

    (1)filter基于filter接口中的doFilter回调函数,interceptor则基于Java本身的反射机制;

    比如HandlerInterceptor是交给spring bean工厂去反射生成的。

    (2)filter是依赖于servlet容器的,没有servlet容器就无法回调doFilter方法,而interceptor与servlet无关;

    (3)filter的过滤范围比interceptor大,filter除了过滤请求外通过通配符可以保护页面、图片、文件等,而interceptor只能过滤请求,只对controller起作用,在controller之前开始,在controller完成后结束(如被拦截,不执行action);

    (4)filter的过滤一般在加载的时候在init方法声明,而interceptor可以通过在xml声明是guest请求还是user请求来辨别是否过滤;

    (5)interceptor可以访问controller上下文、值栈里的对象,而filter不能;

    (6)在controller的生命周期中,拦截器可以被多次调用,而过滤器只能在容器初始化时被调用一次。


    源码下载:SpringMVC的HandlerInterceptor(Session和Cookie登录认证)

    好了,JavaWeb--Servlet过滤器Filter和SpringMVC的HandlerInterceptor(Session和Cookie登录认证)讲完了,这是项目过程中一个必须登录认证机制,现在罗列给大家,这是积累的必经一步,我会继续出这个系列文章,分享经验给大家。欢迎在下面指出错误,共同学习!!你的点赞是对我最好的支持!!

    更多内容,可以访问JackFrost的博客

    相关文章

      网友评论

        本文标题:JavaWeb--Servlet过滤器Filter和Spring

        本文链接:https://www.haomeiwen.com/subject/fljrtxtx.html