Filter
過濾器(Filter)可以修改HTTP請求的內容、響應、Header等信息,過濾器可以包裝請求、響應,比如防止XSS攻擊等,過濾器同樣也可以攔截不安全的請求,比如防止CSRF攻擊等等。
生命周期
Filter生命周期與Servlet生命周期類似,init()初始化Filter、destory()在銷毀時調用、doFilter()負責處理過濾響應和請求。
包裝響應、請求
Filter最核心的概念就是包裝請求或響應,以便它可以執行新的行為。Servlet提供HttpServletRequestWrapper、HttpServletResponseWrapper對象進行包裝請求和響應,使用時直接繼承即可。
下面是防止XSS攻擊,進行請求包裝
private static class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {
XssHttpServletRequestWrapper(HttpServletRequest servletRequest) {
super(servletRequest);
}
@Override
public String getHeader(String name) {
return HtmlUtils.htmlEscape(super.getHeader(name));
}
@Override
public String getQueryString() {
return HtmlUtils.htmlEscape(super.getQueryString());
}
@Override
public String getParameter(String parameter) {
return HtmlUtils.htmlEscape(super.getParameter(parameter));
}
@Override
public String[] getParameterValues(String parameter) {
String[] values = super.getParameterValues(parameter);
if (values == null) {
return null;
}
for (int i = 0; i < values.length; i++) {
values[i] = HtmlUtils.htmlEscape(values[i]);
}
return values;
}
@Override
public Map<String, String[]> getParameterMap() {
Map<String, String[]> paramMap = new HashMap<>(super.getParameterMap());
for (Map.Entry<String, String[]> entry : paramMap.entrySet()) {
String[] values = entry.getValue();
String[] after = new String[values.length];
int index = 0;
for (String value : values) {
after[index++] = HtmlUtils.htmlEscape(value);
}
entry.setValue(after);
}
return paramMap;
}
}
}
Spring Session就是使用Wrapper把獲取Session的API進行包裝,部分代碼如下:
public class SessionRepositoryFilter<S extends ExpiringSession> extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);
SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(
request, response, this.servletContext);
SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper(
wrappedRequest, response);
HttpServletRequest strategyRequest = this.httpSessionStrategy
.wrapRequest(wrappedRequest, wrappedResponse);
HttpServletResponse strategyResponse = this.httpSessionStrategy
.wrapResponse(wrappedRequest, wrappedResponse);
try {
filterChain.doFilter(strategyRequest, strategyResponse);
}
finally {
wrappedRequest.commitSession();
}
}
}
Filter和RequestDispatcher
從Servlet2.4之后我們可用使用forward()和include()進行請求分派,同樣Filter同樣可以攔截分派的請求。
在配置Filter-Mapping時有
- REQUEST:攔截客戶端請求,Filter-Mapping默認就是該類型
- FORWARD:攔截forward()分派請求
- INCLUDE:攔截include()分派請求
- ASYNC:攔截異步請求
- ERROR:攔截錯誤請求
配置如下
<filter>
<filter-name>corsFilter</filter-name>
<filter-class>com.kanyuxia.servlet.chapter.filter.CrossOriginFilter</filer-class>
</filter>
<filter-mapping>
<filter-name>corsFilter</filter-name>
<url-pattern>/*</url-pattern>
<dispatcher>FORWARD</dispatcher>
</filter-mapping>
Listener
事件監聽器能夠控制ServletContext、HttpSession和ServletRequest的生命周期相關的活動
監聽器接口 | 監聽器事件 |
---|---|
ServletContextListener | ServletContextEvent |
ServletContextAttributeListener | ServletContextAttributeEvent |
HttpSessionListener | HttpSessionEvent |
HttpSessionAttributeListener | HttpSessionBindingEvent |
HttpSessionIdListener | HttpSessionEvent |
HttpSessionActivationListener | HttpSessionEvent |
HttpSessionBindingListener | HttpSessionBindingEvent |
ServletRequestListener | ServletRequestEvent |
ServletRequestAttributeListener | ServletRequestAttributeEvent |
AsyncListener | AsyncEvent |
監聽器的常見應用於其控制的相關對象的生命周期,我們可以基於此讓所有請求入庫
@WebListener
public class AccessManager implements ServletRequestListener {
@Override
public void requestInitialized(ServletRequestEvent requestEvent) {
ServletContext context = requestEvent.getServletContext();
ConnectionPool connectionPool = (ConnectionPool) context.getAttribute(ConnectionManager.CONNECTION_POOL_NAME);
HttpServletRequest request = (HttpServletRequest)requestEvent.getServletRequest();
recordAccessLog(connectionPool, request);
}
@Override
public void requestDestroyed(ServletRequestEvent requestEvent) {}
private void recordAccessLog(ConnectionPool connectionPool, HttpServletRequest request) {
// 省略部分代碼邏輯
}
}
Cookie和Session
由於HTTP是無狀態的基於請求/響應模式的協議。在構建有效的Web應用,必須與來自特定客戶端的請求彼此相互關聯,就是會話跟蹤機制。會話跟蹤機制有cookie-session、無狀態的JWT、token-session機制,這里主要說的是cookie-session會話機制。
Cookie和Session在Servlet中如何使用就不說了,這里主要說一下自己在應用過程中遇到的問題
- Cookie的domain:Cookie中的domain指的是該cookie在該domain(域名或IP地址)下有效,在瀏覽器中只能看到domian下的cookie。
- Cookie的http-only:Cookie中的Http-Only選項指的是該Cookie是否只能Http請求使用,主要是防止CSRF攻擊。
- 分布式下的Session:由於Session代表用戶,所以每個用戶應該有唯一的Session。一般情況下,分布式環境下集中存放Session,例如使用Redis集中存放Session,所以就出現了Spring Session進行集中式存放Session。
映射到Servlet
Servlet容器在接受到HTTP請求后,需要選擇合適的Servlet處理該請求。選擇的Servlet根據URL匹配最長上下文路徑的Servlet,其中"*"代表匹配任意字符串,最后我們發現如果"/"會匹配任意的請求。"/"代表"default"的Servlet,Servlet容器會自動注入一個匹配路徑為"/"的默認的Servlet,處理靜態文件獲取、404錯誤等等。
這是Tomcat9.0.2注入的默認Servlet部分代碼
public class DefaultServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request,
HttpServletResponse response) throws IOException, ServletException {
// Serve the requested resource, including the data content
serveResource(request, response, true, fileEncoding);
}
/**
* Serve the specified resource, optionally including the data content.
*/
protected void serveResource(HttpServletRequest request,
HttpServletResponse response, boolean content,
String inputEncoding) throws IOException, ServletException {
// 省略代碼
}
}