什麼是過濾器
過濾器是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