Filter:過濾器
引言
我們可以通過使用前面的技術,做出一些簡單的登陸注冊以及配合數據庫實現對數據增刪改查的Demo,程序是基本運行起來了,但是卻存在着一個重大的安全問題,那就登陸權限驗證,一般來說登陸的正確流程是這樣的:用戶在客戶端發出請求 -> 后台判斷是否登錄 -> 是則不限制,否則 跳轉回登錄頁面,判斷是否登錄和我們前面所學習的 Header中獲取referer再判斷達從而到防盜鏈的效果有相似的感覺,就是起一個判斷過濾的樣子,而Filter則是一個更好的解決這樣問題的技術,當然強大的功能不止這一點,下面我們就好好來說一說!
(一) 過濾器概述
過濾器,顧名思義就是起到過濾篩選作用的一種事物,只不過相較於現實生活中的過濾器,這里的過濾器過濾的對象是客戶端訪問的web資源,也可以理解為一種預處理手段,對資源進行攔截后,將其中我們認為的雜質(用戶自己定義的)過濾,符合條件的放行,不符合的則攔截下來
當然,過濾器既可以攔截request,也可以攔截返回的response,我們來看一張圖
(二) 第一個過濾器程序
過濾器的本質就是一個實現了 Filter 接口的 Java 類
我們先自己創建一個類,實現Filter接口(javax.servlet),重寫其中的所有方法
@WebFilter("/*")
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
(三) filter配置
第一種:web.xml配置
<filter>
<filter-name>filterDemo1</filter-name>
<filter-class>package cn.ideal.web.filter.FilterDemo1</filter-class>
</filter>
<filter-mapping>
<filter-name>filterDemo1</filter-name>
<!-- 攔截路徑 -->
<url-pattern>/*</url-pattern>
</filter-mapping>
filter
<filter-name></filter-name>
:指定filter名字
<filter-class></filter-class>
:指定filter全類名(帶包名)
filter-mapping
<filter-name></filter-name>
:這里的標簽是為了與上面filter中的名字對應,從而指向到對應的文件中
<url-pattern></url-pattern>
:設置filter所攔截的路徑 ※ 這里決定了什么樣的資源會被過濾器攔截處理
攔截路徑設置
格式 | 解釋 |
---|---|
/test.jsp | 只有訪問test.jsp這個資源的時候才會執行過濾器 |
/test/* | 訪問test下所有資源你的時候,執行過濾器 |
*.jsp | 所有jsp格式的資源被訪問的時候,執行過濾器 |
/* | 任意資源被訪問,均執行過濾器 |
由於過濾器內設置的是比較通用的一些設置,所以一般來說使用 /*
這種格式,不過也可以根據需求情況選擇
攔截方式配置:dispatcher
攔截方式配置也就是資源被訪問的形式,有這么幾個屬性
-
REQUEST:默認值,瀏覽器直接請求資源
-
FORWARD:轉發訪問資源 : RequestDispatcher.forward();
-
INCLUDE:包含訪問資源 : RequestDispatcher.include();
-
ERROR:錯誤跳轉資源 : 被聲明式異常處理機制調用的時候
補充:聲明式異常處理即:在web.xml中通過配置來確定不同的異常類型將如何被處理,最后跳轉到哪個頁面,也就是我們常常看到的一些404錯誤頁面
<error-page>
<!--異常的類-->
<exception-type>xxx</exception-type>
<!--異常發生時跳轉的頁面-->
<location>xxx</location>
</error-page>
第二種:使用注解配置
與servlet相似的配置 ,我們可以指定它的名字和攔截路徑
@WebFilter("filterName="FilterDemo1",urlPatters="/*")
但是直接在類上聲明注解,顯然那我們是不需要指定其名字的,而通過查看源碼又可以知道,urlPatters又可以被value指定,而value又可以省略,所以我們可以簡寫為
@WebFilter("/*")
若想在filter注解中配置dispatcher,我們需要設置dispatcherTypes屬性
@WebFilter(value = "/*",dispatcherTypes ={DispatcherType.FORWARD,DispatcherType.FORWARD} )
(四) 過濾器的生命周期
講完了配置,下面我們就回歸主題來說一說過濾器的生命周期,也就是上面實現接口而重寫的那些方法們
首先是 init(FilterConfig config)
方法和 void destroy()
方法,Servlet也有這兩個方法,兩者分別在服務器啟動和關閉的時候被創建以及銷毀,兩者均執行一次,用於加載以及釋放資源
其實就這兩個方法來說在Servlet的基礎上還是很好理解的
再者就是我們過濾器的核心方法了:
void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)
doFilter方法就是我們真正進行攔截的方法,通過前兩個參數我們可以知道,不論是Request亦或是Respone我們都可以對其進行過濾操作,那么第三個參數是什么意思呢?
我們打開FilterChain的源碼
public interface FilterChain {
void doFilter(ServletRequest var1, ServletResponse var2) throws IOException, ServletException;
}
嗯!FilterChain是一個接口,接口內也定義了一個doFilter方法,它存在的意義是什呢?
這是一種鏈式結構,我們在這里稱作過濾器鏈,其作用就是為了配置多個過濾器,多個過濾器下的執行流程是這樣的
那么,多個過濾器誰前誰后呢?這還與我們前面的配置有關
- 注解配置:按照類名字符串比較,值小的先執行
- Eg:AFilterDemo 優先於 BFilterDemo
- web.xml配置:
<filter-mapping>
中誰在上面,誰優先執行
過濾器的簡單執行流程
-
執行過濾器
-
執行放行后的資源,可能是下一個過濾器,也可能是web資源(JSP/Servlet)
-
執行過濾器放行代碼
chain.doFilter(req, resp);
下邊的代碼
(五) Filter的應用
(1) 登錄權限驗證
我們前面的的知識已經能簡單的滿足我們對於登錄以及簡單注冊的實現,但是如果我們知道地址,直接通過url訪問一些 資源,很顯然這是很不合理的,所以我們需要對登錄狀態進行驗證,未登錄則轉發到的登錄界面,登錄則可以依據登錄狀態自由訪問一些頁面
我們寫一個簡單的模擬程序,為了可讀性,以及篇幅問題,我們省略數據庫連接的部分,采用固定的密碼
這是index.jsp頁面,也就是需要登錄后才能放開訪問權限的頁面
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>$Title$</title>
</head>
<body>
<h1>這是首頁,只有登錄后才能查看</h1>
</body>
</html>
這是login.jsp頁面,也就是登錄頁面,非常簡單
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<form action="/web-test/loginServlet" method="post">
<table>
<tr>
<td>用戶名:</td>
<td><input type="text" name="username"></td>
</tr>
<tr>
<td>密碼:</td>
<td><input type="password" name="password"></td>
</tr>
<tr>
<td><input type="submit" value="登錄"></td>
</tr>
</table>
</form>
</body>
</html>
我們創一個domain 包,寫一個User實體,補充其get、set方法
package cn.ideal.domain;
public class User {
private String username;
private String password;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String toString() {
return "User{" +
"username='" + username + '\'' +
", password='" + password + '\'' +
'}';
}
}
下面開始編寫LoginServlet,也就是處理登錄驗證問題的代碼
package cn.ideal.web.servlet;
import cn.ideal.dao.UserDao;
import cn.ideal.domain.User;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet("/loginServlet")
public class LoginServlet extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//設置編碼
request.setCharacterEncoding("utf-8");
//獲取請求參數
String username = request.getParameter("username");
String password = request.getParameter("password");
//封裝user對象
User loginUser = new User();
loginUser.setUsername(username);
loginUser.setPassword(password);
UserDao dao = new UserDao();
User user = dao.login(loginUser);
if (user == null){
//登陸失敗
request.getRequestDispatcher("/failServlet").forward(request,response);
}else{
//登錄成功
request.getSession().setAttribute("user",user);
request.getRequestDispatcher("/index.jsp").forward(request,response);
}
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doPost(request,response);
}
}
我們根據 user 是否等於 null來判斷用戶名密碼是否正確,那么我們你就來寫一下這個返回了一個User對象的login方法
我們在dao層中創建一個UserDao類,正式一些的項目會寫成接口的形式,在impl層中再寫實現,為了掩飾我們簡化這一步
package cn.ideal.dao;
import cn.ideal.domain.User;
public class UserDao {
public User login(User loginUser) {
//定義真實用戶名密碼(代替數據庫讀取)
String trueUsername = "admin";
String truePassword = "admin";
if (loginUser.getUsername().equals(trueUsername) && loginUser.getPassword().equals(truePassword)) {
//登陸成功
return loginUser;
} else {
return null;
}
}
}
關鍵來了,這也就是我們所講的過濾器方法,這里所需要注意的就是 登陸成功后,記得寫入狀態
request.getSession().setAttribute("user",user);
package cn.ideal.web.filter;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebFilter("/*")
public class LoginFilter implements Filter {
public void destroy() {
}
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) resp;
//獲取資源請求路徑
String requestURI = request.getRequestURI();
//排除包含登錄確實所需要的資源,給予放行
if (requestURI.contains("/login.jsp") || requestURI.contains("/loginServlet")) {
chain.doFilter(request,response);
}else{
//不包含,即驗證用戶是否已經登錄
Object user = request.getSession().getAttribute("user");
if (user != null){
//登陸了,放行
chain.doFilter(request,response);
}else{
//沒有登錄,跳轉回登錄頁面
request.getRequestDispatcher("/login.jsp").forward(request,response);
}
}
}
public void init(FilterConfig config) throws ServletException {
}
}
(2) 敏感詞過濾
如果我們想要對用戶提交的一些信息進行過濾,在servlet中進行一些代碼的編寫也算一種方法,但是最為合適的還是fiter,它更加通用,下面我們使用代理模式增強request從而使用filter進行敏感詞的過濾
我們就在剛才的index頁面上加以修改
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>$Title$</title>
</head>
<body>
<h1>這是首頁,只有登錄后才能查看</h1>
<form action="/web-test/replaceServlet" method="post">
<table>
<tr>
<td><input type="text" name="words"></td>
</tr>
<tr>
<td><input type="submit" value="敏感字檢測"></td>
</tr>
</table>
</form>
</body>
</html>
我們把傳入的參數讀取進來
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
request.setCharacterEncoding("UTF-8");
String words = request.getParameter("words");
System.out.println(words);
}
package cn.ideal.web.filter;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.*;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.List;
@WebFilter("/*")
public class ReplaceFilter implements Filter {
public void destroy() {
}
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {
//創建代理對象,增強getParameter
ServletRequest proxy_req = (ServletRequest) Proxy.newProxyInstance(req.getClass().getClassLoader(), req.getClass().getInterfaces(), new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//判斷是不是getParameter方法
if (method.getName().equals("getParameter")){
//獲取返回值
String value = (String)method.invoke(req, args);
if (value != null){
for (String s : list){
if (value.contains(s)){
value = value.replaceAll(s,"***");
}
}
}
return value;
}
return method.invoke(req,args);
}
});
chain.doFilter(proxy_req, resp);
}
private List<String> list = new ArrayList<String>();
public void init(FilterConfig config) throws ServletException {
try {
//獲取文件真實路徑
ServletContext servletContext = config.getServletContext();
String realPath = servletContext.getRealPath("/WEB-INF/classes/replace.txt");
//讀取文件
BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(realPath),"UTF-8"));
//將每一行數據添加到list中
String line = null;
while((line = br.readLine())!=null){
list.add(line);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
結尾:
如果內容中有什么不足,或者錯誤的地方,歡迎大家給我留言提出意見, 蟹蟹大家 !_
如果能幫到你的話,那就來關注我吧!(系列文章均會在公眾號第一時間更新)
在這里的我們素不相識,卻都在為了自己的夢而努力 ❤
一個堅持推送原創Java技術的公眾號:理想二旬不止