@[TOC](Servlet - Filtering (过滤器) )
1. What
1.1 什么是Filter
Servlet过滤器Filter是一个小型的web组件,它们通过拦截请求和响应,以便查看、提取或以某种方式操作客户端和服务器之间交换的数据,实现“过滤”的功能。Filter通常封装了一些功能的web组件,过滤器提供了一种面向对象的模块化机制,将任务封装到一个可插入的组件中, Filter组件通过配置文件来声明,并动态的代理。
1.2 Filter与Interceptor的区别
- 作用域不同:
过滤器依赖于servlet容器,只能在 servlet容器,web环境下使用
拦截器依赖于spring容器,可以在spring容器中调用,不管此时spring处于什么环境 - 细粒度的不同:
过滤器的控制比较粗,只能在请求进来时进行处理,对请求和响应进行包装
拦截器提供更精细的控制,可以在controller对请求处理之前或之后被调用,也可以在渲染视图呈现给用户之后调用 - 中断链执行的难易程度不同:
拦截器可以 preHandle方法内返回 false 进行中断
过滤器就比较复杂,需要处理请求和响应对象来引发中断,需要额外的动作,比如将用户重定向到错误页面
简单总结一下,拦截器相比过滤器有更细粒度的控制,依赖于spring容器,可以在请求之前或之后启动,过滤器主要依赖于servlet,过滤器能做的,拦截器基本上都能做
2. Why
2.1 Filter的作用
它的主要作用就是将请求进行过滤处理然后将过滤后的请求交给下一个资源。其本质是Web应用的一个组成部件,承担了Web应用安全的部分功能,阻止不合法的请求和非法的访问。
2.2 Filter的适用场合
- 将请求的参数写入日志文件
- 对于资源的访问进行统一的授权与验证
- 在请求到达实际Servlet之前格式化请求内容或请求头
- 压缩返回数据后发送给客户端
- 修改返回内容,增加一些cookie、header等信息
3. How
3.1 Interface of Filter
public interface Filter {
default void init(FilterConfig paramFilterConfig) throws ServletException;
void doFilter(ServletRequest paramServletRequest, ServletResponse paramServletResponse, FilterChain
paramFilterChain) throws IOException, ServletException;
default void destroy();
}
过滤器是一个实现了javax.servlet.Filter接口的Java类。javax.servlet.Filter接口定义了三个方法:
- public void init(FilterConfig filterConfig) — web应用程序启动时,web服务器将创建Filter 的实例对象,并调用其init方法,读取web.xml配置,完成对象的初始化功能,从而为后续的用户请求作好拦截的准备工作(filter对象只会创建一次,-
init方法也只会执行一次)。开发人员通过init方法的参数,可获得代表当前filter配置信息的FilterConfig对象。 - public void doFilter (ServletRequest, ServletResponse, FilterChain) — 该方法完成实际的过滤操作,当客户端请求方法与过滤器设置匹配的URL时,Servlet容器将先调用过滤器的doFilter方法。FilterChain用户访问后续过滤器。
- public void destroy() — Servlet容器在销毁过滤器实例前调用该方法,在该方法中释放Servlet过滤器占用的资源。
3.2 Filter的工作原理与执行顺序
Filter接口中有一个doFilter方法,当我们编写好Filter,并配置对哪个web资源进行拦截后,WEB服务器每次在调用web资源的service方法之前,都会先调用一下filter的doFilter方法,因此,在该方法内编写代码可达到如下目的:
- 调用目标资源之前,让一段代码执行;
- 是否调用目标资源(即是否让用户访问web资源);
- 调用目标资源之后,让一段代码执行。
web服务器在调用doFilter方法时,会传递一个filterChain对象进来,filterChain对象是filter接口中最重要的一个对象,它也提供了一个doFilter方法,开发人员可以根据需求决定是否调用此方法,调用该方法,则web服务器就会调用web资源的service方法,即web资源就会被访问,否则web资源不会被访问。
3.3 Filter的责任链设计
责任链定义:为了避免请求发送者与多个请求处理者耦合在一起,于是将所有请求的处理者通过前一对象记住其下一个对象的引用而连成一条链;当有请求发生时,可将请求沿着这条链传递,直到有对象处理它为止。
每个Filter过滤器实现javax.servlet.Filter接口,其中包含一个doFilter()方法,该方法接受一个request,resonse以及filterChain作为参数输入,filterChain实现javax.servlet.FilterChain接口(由servlet容器提供),当请求到来时,它将会管理与该请求相关的一系列过滤器的执行,当过滤器执行完毕,doFilter接下来就会调用servlet的service()方法。
责任链模式解决的问题 :
- 请求者和接受者松散耦合 — 在责任链模式中,请求者并不知道接受者是谁,也不知道具体如何处理。请求者只负责向责任链发出请求就可以了,该模式下可以有多个接受者处理对象,每个接受者只负责处理自己的部分,其他的就交给其他的接受者去处理。请求在链中传递,接受者处理该请求,或者传递给链中下一个接受者。请求者不再和特定接受者紧密耦合。
- 通过改变链内成员的或者调整它们的次序,允许你动态地新增或删除责任。
4. Samples
4.1 权限控制
public class AdminFilter implements Filter {
private final String[] excludedPathArray = new String[]{
"/static/*", "*.html", "*.js", "*.ico", "*.jpg", "*.png", "*.css"
};
/**
* doFilter Dedicated to intercepting requests. Can do permission checking
*/
@Override
public void doFilter(ServletRequest servletRequest,
ServletResponse servletResponse,
FilterChain filterChain) throws ServletException, IOException {
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
if (!isFilterExcludeRequest(httpServletRequest)) {
HttpSession session = httpServletRequest.getSession();
Object user = session.getAttribute("user");
// If it is equal to null, it means that you are not logged in yet
if (user == null) {
// TODO do business logic
} else {
// Let the program continue down to access the user's target resource
filterChain.doFilter(servletRequest, servletResponse);
}
}
}
/**
* Determine if the request is directly released by the filter (mainly used for static resource release)
*
* @param request HTTP request
* @return boolean
*/
private boolean isFilterExcludeRequest(HttpServletRequest request) {
if (excludedPathArray.length > 0) {
String url = request.getRequestURI();
for (String excludedUrl : excludedPathArray) {
if (excludedUrl.startsWith("*.")) {
if (url.endsWith(excludedUrl.substring(1))) {
return true;
}
} else if (excludedUrl.endsWith("/*")) {
if (!excludedUrl.startsWith("/")) {
excludedUrl = "/" + excludedUrl;
}
String prefixStr = request.getContextPath() +
excludedUrl.substring(0, excludedUrl.length() - 1);
if (url.startsWith(prefixStr)) {
return true;
}
} else {
if (!excludedUrl.startsWith("/")) {
excludedUrl = "/" + excludedUrl;
}
String targetUrl = request.getContextPath() + excludedUrl;
if (url.equals(targetUrl)) {
return true;
}
}
}
}
return false;
}
}
4.2 XM:config Filter by 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" version="3.0">
<display-name>ServletFilterExample</display-name>
<filter>
<filter-name>adminFilter</filter-name>
<filter-class>com.xxx.AdminFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>adminFilter</filter-name>
<!-- the rule of filter URL-->
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>
4.3 onfig Filter by Java config
@Configuration
@ConditionalOnClass(name = {
"org.springframework.web.servlet.config.annotation.WebMvcConfigurer",
"org.springframework.boot.web.servlet.FilterRegistrationBean"
})
public class FilterConfiguration {
@Bean
public FilterRegistrationBean<Filter> traceableFilter() {
FilterRegistrationBean<Filter> registration = new FilterRegistrationBean<>();
registration.setFilter(new AdminFilter());
// 设定优先值为最优先
registration.setOrder(Integer.MIN_VALUE);
return registration;
}
}
4.4 使用Filter对Token进行拦截验证
对于一些敏感数据,在用户每次请求时,都需要对用户的身份进行验证。这里我们使用JWT的token进行身份认证。
- 新定义一个拦截器
public class TokenVerifyFilter extends OncePerRequestFilter
OncePerRequestFilter是由Spring.web提供的一个拦截器,它内部定义了一些列框架需要的数据验证流程。
public abstract class OncePerRequestFilter extends GenericFilterBean {
...
@Override
public final void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
...
// Do invoke this filter...
request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE);
try {
doFilterInternal(httpRequest, httpResponse, filterChain);
} finally {
// Remove the "already filtered" request attribute for this request.
request.removeAttribute(alreadyFilteredAttributeName);
}
....
}
...
}
- 上述代码中,方法doFilterInternal是一个抽象方法,我们需要在自己定义的TokenVerifyFilter实现这个方法。在实现的代码里,添加对token验证的逻辑
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
...
// get token from the request
String tokenString = request.getHeader(Constants.ACCESS_TOKEN);
// If the token does not exist, continue to execute the doFilter method in OncePerRequestFilter
if (!StringUtils.hasText(tokenString)) {
filterChain.doFilter(request, response);
return;
}
...
// Execute token verification logic
try {
varifyToken(tokenString);
} catch (Exception e) {
... // Other operations: logging, assembly of error message objects returned to the caller, etc.
return;
}
// After the token verification is passed, continue to execute the doFilter method in OncePerRequestFilter
filterChain.doFilter(wrapperRequest, response);
...
}
4.5 使用Filter对marketCode和languageCode进行拦截验证
- Define a Filter called MarketCodeAndLanguageCodeFilter
@WebFilter(urlPatterns = "/api/*")
@RequiredArgsConstructor
public class MarketCodeAndLanguageCodeFilter extends OncePerRequestFilter {
...
}
@WebFilter(urlPatterns = "/api/") 表示,此拦截器只处理符合正则 /api/ 的 url
@WebFilter(urlPatterns = "/api/") means that this blocker only processes urls that match the regular /api/
- 实现 doFilterInternal 方法
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
// Check that marketCode is not empty, otherwise return an error message directly
String marketCode = request.getHeader(MARKET_CODE);
if (!MARKET_CODE_LIST.contains(marketCode)) {
resolveException(HttpStatus.BAD_REQUEST.value(), MARKET_CODE_INVALID.getStatusCode(),
response);
return;
}
// The same logic handles languageCode
...
// store the marketCode and languageCode to the ContextUtil(it's a thread-local)
contextUtil.put(MARKET_CODE, marketCode);
contextUtil.put(LANGUAGE_CODE, languageCode);
try {
// Continue with subsequent operations
filterChain.doFilter(request, response);
} finally {
// When the entire request is complete, empty the ContextUtil
contextUtil.clear();
}
}