攔截一些請求進行處理,比如通過它來進行權限驗證,或者是來判斷用戶是否登陸,日志記錄,編碼,或者限制時間點訪問等等,是非常有必要的。所以就有了此篇文章啦。
文章結構:(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&×tamp!=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。結果如下:默認返回登錄頁面(因為我們攔截重定向了)
這里寫圖片描述