過濾器入門看這一篇就夠了

Java3y發表於2018-02-08

什麼是過濾器

過濾器是Servlet的高階特性之一,也別把它想得那麼高深,只不過是實現Filter介面的Java類罷了!

首先,我們來看看過濾器究竟Web容器的哪處:

過濾器入門看這一篇就夠了

從上面的圖我們可以發現,當瀏覽器傳送請求給伺服器的時候,先執行過濾器,然後才訪問Web的資源。伺服器響應Response,從Web資源抵達瀏覽器之前,也會途徑過濾器。。

我們很容易發現,過濾器可以比喻成一張濾網。我們想想現實中的濾網可以做什麼:在泡茶的時候,過濾掉茶葉。那濾網是怎麼過濾茶葉的呢?規定大小的網孔,只要網孔比茶葉小,就可以實現過濾了!

引申在Web容器中,過濾器可以做:過濾一些敏感的字串【規定不能出現敏感字串】、避免中文亂碼【規定Web資源都使用UTF-8編碼】、許可權驗證【規定只有帶Session或Cookie的瀏覽器,才能訪問web資源】等等等,過濾器的作用非常大,只要發揮想象就可以有意想不到的效果

也就是說:當需要限制使用者訪問某些資源時、在處理請求時提前處理某些資源、伺服器響應的內容對其進行處理再返回、我們就是用過濾器來完成的!


為什麼需要用到過濾器

直接舉例子來說明吧:

沒有過濾器解決中文亂碼問題

  • 如果我沒有用到過濾器:瀏覽器通過http請求傳送資料給Servlet,如果存在中文,就必須指定編碼,否則就會亂碼!

  • jsp頁面提交中文資料給Servlet處理


<form action="${pageContext.request.contextPath}/Demo1" method="post">

    <input type="text" name="username">

    <input type="submit" value="提交">

</form>

複製程式碼
  • Servlet沒有指定編碼的情況下,獲取得到的是亂碼

過濾器入門看這一篇就夠了

Servlet中如何解決中文亂碼問題,我的其他博文中有:blog.csdn.net/hon_3y/arti…

也就是說:如果我每次接受客戶端帶過來的中文資料,在Serlvet中都要設定編碼。這樣程式碼的重複率太高了!!!!


有過濾器解決中文亂碼問題

有過濾器的情況就不一樣了:只要我在過濾器中指定了編碼,可以使全站的Web資源都是使用該編碼,並且重用性是非常理想的!


過濾器 API

只要Java類實現了Filter介面就可以稱為過濾器!Filter介面的方法也十分簡單:

過濾器入門看這一篇就夠了

其中init()和destory()方法就不用多說了,他倆跟Servlet是一樣的。只有在Web伺服器載入和銷燬的時候被執行,只會被執行一次!

值得注意的是doFilter()方法,**它有三個引數(ServletRequest,ServletResponse,FilterChain),**從前兩個引數我們可以發現:過濾器可以完成任何協議的過濾操作

那FilterChain是什麼東西呢?我們看看:

過濾器入門看這一篇就夠了

FilterChain是一個介面,裡面又定義了doFilter()方法。這究竟是怎麼回事啊??????

我們可以這樣理解:過濾器不單單隻有一個,那麼我們怎麼管理這些過濾器呢?在Java中就使用了鏈式結構把所有的過濾器都放在FilterChain裡邊,如果符合條件,就執行下一個過濾器(如果沒有過濾器了,就執行目標資源)

上面的話好像有點拗口,我們可以想象生活的例子:現在我想在茶杯上能過濾出石頭和茶葉出來。石頭在一層,茶葉在一層。所以茶杯的過濾裝置應該有兩層濾網。這個過濾裝置就是FilterChain,過濾石頭的濾網和過濾茶葉的濾網就是Filter。在石頭濾網中,茶葉是屬於下一層的,就把茶葉放行,讓茶葉的濾網過濾茶葉。過濾完茶葉了,剩下的就是茶(茶就可以比喻成我們的目標資源)


快速入門

寫一個簡單的過濾器

  • 實現Filter介面的Java類就被稱作為過濾器

	public class FilterDemo1 implements Filter {
	    public void destroy() {
	    }
	
	    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {
	
	        //執行這一句,說明放行(讓下一個過濾器執行,如果沒有過濾器了,就執行執行目標資源)
	        chain.doFilter(req, resp);
	    }
	
	    public void init(FilterConfig config) throws ServletException {
	        
	    }
	}

複製程式碼

filter部署

過濾器和Servlet是一樣的,需要部署到Web伺服器上的。

第一種方式:在web.xml檔案中配置

filter

<filter>用於註冊過濾器


	<filter>
	 	     <filter-name>FilterDemo1</filter-name>
		     <filter-class>FilterDemo1</filter-class>
		     <init-param>
			 <param-name>word_file</param-name>	
			 <param-value>/WEB-INF/word.txt</param-value>
		     </init-param>
	</filter>


複製程式碼
  • <filter-name>用於為過濾器指定一個名字,該元素的內容不能為空。
  • <filter-class>元素用於指定過濾器的完整的限定類名
  • <init-param>元素用於為過濾器指定初始化引數,它的子元素指定引數的名字,<param-value>指定引數的值。在過濾器中,可以使用FilterConfig介面物件來訪問初始化引數

filter-mapping

<filter-mapping>元素用於設定一個Filter 所負責攔截的資源

一個Filter攔截的資源可通過兩種方式來指定:Servlet 名稱和資源訪問的請求路徑



    <filter-mapping>
        <filter-name>FilterDemo1</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

複製程式碼
  • <filter-name>子元素用於設定filter的註冊名稱。該值必須是在元素中宣告過的過濾器的名字
  • <url-pattern>設定 filter 所攔截的請求路徑(過濾器關聯的URL樣式)
  • <servlet-name>指定過濾器所攔截的Servlet名稱
  • <dispatcher>指定過濾器所攔截的資源被 Servlet 容器呼叫的方式,可以是REQUEST,INCLUDE,FORWARD和ERROR之一,預設REQUEST。使用者可以設定多個<dispatcher> 子元素用來指定 Filter 對資源的多種呼叫方式進行攔截。

dispatcher

子元素可以設定的值及其意義:

  • REQUEST:當使用者直接訪問頁面時,Web容器將會呼叫過濾器。如果目標資源是通過RequestDispatcher的include()或forward()方法訪問時,那麼該過濾器就不會被呼叫。
  • INCLUDE:如果目標資源是通過RequestDispatcher的include()方法訪問時,那麼該過濾器將被呼叫。除此之外,該過濾器不會被呼叫。
  • FORWARD:如果目標資源是通過RequestDispatcher的forward()方法訪問時,那麼該過濾器將被呼叫,除此之外,該過濾器不會被呼叫。
  • ERROR:如果目標資源是通過宣告式異常處理機制呼叫時,那麼該過濾器將被呼叫。除此之外,過濾器不會被呼叫。

第二種方式:通過註解配置



	@WebFilter(filterName = "FilterDemo1",urlPatterns = "/*")


複製程式碼

上面的配置是“/*”,所有的Web資源都需要途徑過濾器

如果想要部分的Web資源進行過濾器過濾則需要指定Web資源的名稱即可!


過濾器的執行順序

上面已經說過了,過濾器的doFilter()方法是極其重要的,FilterChain介面是代表著所有的Filter,FilterChain中的doFilter()方法決定著是否放行下一個過濾器執行(如果沒有過濾器了,就執行目標資源)

測試一

  • 首先在過濾器的doFilter()中輸出一句話,並且呼叫chain物件的doFilter()方法

    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {

        System.out.println("我是過濾器1");

        //執行這一句,說明放行(讓下一個過濾器執行,或者執行目標資源)
        chain.doFilter(req, resp);
    }

複製程式碼
  • 我們來訪問一下test.jsp頁面:

過濾器入門看這一篇就夠了

我們發現test.jsp(我們的目標資源)成功訪問到了,並且在伺服器上也列印了字串!


測試二

我們來試試把chain.doFilter(req, resp);這段程式碼註釋了看看!

過濾器入門看這一篇就夠了

test.jsp頁面並沒有任何的輸出(也就是說,並沒有訪問到jsp頁面)。


測試三

直接看下面的程式碼。我們已經知道了”準備放行“會被列印在控制檯上和test.jsp頁面也能被訪問得到,但“放行完成“會不會列印在控制檯上呢?


    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {

        System.out.println("準備放行");

        //執行這一句,說明放行(讓下一個過濾器執行,或者執行目標資源)
        chain.doFilter(req, resp);

        System.out.println("放行完成");
    }

複製程式碼

答案也非常簡單,肯定會列印在控制檯上的。我們來看看:

過濾器入門看這一篇就夠了

注意,它的完整流程順序是這樣的:客戶端傳送http請求到Web伺服器上,Web伺服器執行過濾器,執行到”準備放行“時,就把字串輸出到控制檯上,接著執行doFilter()方法,Web伺服器發現沒有過濾器了,就執行目標資源(也就是test.jsp)。目標資源執行完後,回到過濾器上,繼續執行程式碼,然後輸出”放行完成“

測試四

我們再多加一個過濾器,看看執行順序。

  • 過濾器1

        System.out.println("過濾器1開始執行");

        //執行這一句,說明放行(讓下一個過濾器執行,或者執行目標資源)
        chain.doFilter(req, resp);

        System.out.println("過濾器1開始完畢");

複製程式碼
  • 過濾器2

        System.out.println("過濾器2開始執行");
        chain.doFilter(req, resp);
        System.out.println("過濾器2開始完畢");

複製程式碼
  • Servlet


        System.out.println("我是Servlet1");

複製程式碼

當我們訪問Servlet1的時候,看看控制檯會出現什麼:

過濾器入門看這一篇就夠了

執行順序是這樣的:先執行FilterDemo1,放行,執行FilterDemo2,放行,執行Servlet1,Servlet1執行完回到FilterDemo2上,FilterDemo2執行完畢後,回到FilterDemo1上


注意:過濾器之間的執行順序看在web.xml檔案中mapping的先後順序的,如果放在前面就先執行,放在後面就後執行!如果是通過註解的方式配置,就比較urlPatterns的字串優先順序


Filter簡單應用

  • filter的三種典型應用:
  • 1、可以在filter中根據條件決定是否呼叫chain.doFilter(request, response)方法,即是否讓目標資源執行
  • 2、在讓目標資源執行之前,可以對request\response作預處理,再讓目標資源執行
  • 3、在目標資源執行之後,可以捕獲目標資源的執行結果,從而實現一些特殊的功能

禁止瀏覽器快取所有動態頁面


    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {

        //讓Web資源不快取,很簡單,設定http中response的請求頭即可了!

        //我們使用的是http協議,ServletResponse並沒有能夠設定請求頭的方法,所以要強轉成HttpServletRequest

        //一般我們寫Filter都會把他倆強轉成Http型別的
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) resp;

        response.setDateHeader("Expires", -1);
        response.setHeader("Cache-Control", "no-cache");
        response.setHeader("Pragma", "no-cache");

        //放行目標資源的response已經設定成不快取的了
        chain.doFilter(request, response);
    }


複製程式碼
  • 沒有過濾之前,響應頭是這樣的:

過濾器入門看這一篇就夠了

  • 過濾之後,響應頭是這樣的:

過濾器入門看這一篇就夠了


實現自動登陸

開發實體、集合模擬資料庫、Dao

  • 實體:


    private String username ;
    private String password;


    public User() {
    }

    public User(String username, String password) {
        this.username = username;
        this.password = password;
    }

	//各種setter和getter

複製程式碼
  • 集合模擬資料庫

	public class UserDB {
	
	    private static List<User> users = new ArrayList<>();
	
	
	
	    static {
	        users.add(new User("aaa", "123"));
	        users.add(new User("bbb", "123"));
	        users.add(new User("ccc", "123"));
	    }
	
	    public static List<User> getUsers() {
	        return users;
	    }
	
	    public static void setUsers(List<User> users) {
	        UserDB.users = users;
	    }
	}


複製程式碼
  • 開發dao

    public User find(String username, String password) {

        List<User> userList = UserDB.getUsers();

        //遍歷List集合,看看有沒有對應的username和password
        for (User user : userList) {
            if (user.getUsername().equals(username) && user.getPassword().equals(password)) {
                return user;
            }
        }
        return null;
    }


複製程式碼

登陸介面



<form action="${pageContext.request.contextPath}/LoginServlet">

    使用者名稱<input type="text" name="username">
    <br>
    密碼<input type="password" name="password">
    <br>

    <input type="radio" name="time" value="10">10分鐘
    <input type="radio" name="time" value="30">30分鐘
    <input type="radio" name="time" value="60">1小時
    <br>

    <input type="submit" value="登陸">

</form>


複製程式碼

處理登陸的Servlet


        //得到客戶端傳送過來的資料
        String username = request.getParameter("username");
        String password = request.getParameter("password");
        
        UserDao userDao = new UserDao();
        User user = userDao.find(username, password);

        if (user == null) {
            request.setAttribute("message", "使用者名稱或密碼是錯的!");
            request.getRequestDispatcher("/message.jsp").forward(request, response);
        }

        //如果不是為空,那麼在session中儲存一個屬性
        request.getSession().setAttribute("user", user);
        request.setAttribute("message", "恭喜你,已經登陸了!");
        
        //如果想要使用者關閉了瀏覽器,還能登陸,就必須要用到Cookie技術了
        Cookie cookie = new Cookie("autoLogin", user.getUsername() + "." + user.getPassword());

        //設定Cookie的最大宣告週期為使用者指定的
        cookie.setMaxAge(Integer.parseInt(request.getParameter("time")) * 60);
        
        //把Cookie返回給瀏覽器
        response.addCookie(cookie);
        
        //跳轉到提示頁面
        request.getRequestDispatcher("/message.jsp").forward(request, response);

複製程式碼

過濾器


        HttpServletResponse response = (HttpServletResponse) resp;
        HttpServletRequest request = (HttpServletRequest) req;

        //如果使用者沒有關閉瀏覽器,就不需要Cookie做拼接登陸了
        if (request.getSession().getAttribute("user") != null) {
            chain.doFilter(request, response);
            return;
        }

        //使用者關閉了瀏覽器,session的值就獲取不到了。所以要通過Cookie來自動登陸
        Cookie[] cookies = request.getCookies();
        String value = null;
        for (int i = 0; cookies != null && i < cookies.length; i++) {
            if (cookies[i].getName().equals("autoLogin")) {
                value = cookies[i].getValue();
            }
        }

        //得到Cookie的使用者名稱和密碼
        if (value != null) {

            String username = value.split("\\.")[0];
            String password = value.split("\\.")[1];

            UserDao userDao = new UserDao();
            User user = userDao.find(username, password);

            if (user != null) {
                request.getSession().setAttribute("user", user);
            }
        }
        
        chain.doFilter(request, response);

複製程式碼
  • 效果:

過濾器入門看這一篇就夠了


改良

我們直接把使用者名稱和密碼都放在了Cookie中,這是明文的。懂點程式設計的人就會知道你的賬號了。

於是乎,我們要對密碼進行加密!


        Cookie cookie = new Cookie("autoLogin", user.getUsername() + "." + md5.md5(user.getPassword()));


複製程式碼
  • 在過濾器中,加密後的密碼就不是資料庫中的密碼的。所以,我們得在Dao新增一個功能【根據使用者名稱,找到使用者】

    public User find(String username) {
        List<User> userList = UserDB.getUsers();

        //遍歷List集合,看看有沒有對應的username和password
        for (User user : userList) {
            if (user.getUsername().equals(username)) {
                return user;
            }
        }

        return null;
    }

複製程式碼
  • 在過濾器中,比較Cookie帶過來的md5密碼和在資料庫中獲得的密碼(也經過md5)是否相同

        //得到Cookie的使用者名稱和密碼
        if (value != null) {

            String username = value.split("\\.")[0];
            String password = value.split("\\.")[1];

            //在Cookie拿到的密碼是md5加密過的,不能直接與資料庫中的密碼比較
            UserDao userDao = new UserDao();
            User user = userDao.find(username);

            //通過使用者名稱獲得使用者資訊,得到使用者的密碼,使用者的密碼也md5一把

            String dbPassword = md5.md5(user.getPassword());
            //如果兩個密碼匹配了,就是正確的密碼了
            if (password.equals(dbPassword)) {
                request.getSession().setAttribute("user", user);
            }

        }

複製程式碼

如果文章有錯的地方歡迎指正,大家互相交流。習慣在微信看技術文章的同學,可以關注微信公眾號:Java3y

相關文章