共计 4199 个字符,预计需要花费 11 分钟才能阅读完成。
在 Spring MVC 中,DispatcherServlet
只需要固定配置到 web.xml
中,剩下的工作主要是专注于编写 Controller。
但是,在 Servlet 规范中,我们还可以使用 Filter。如果要在 Spring MVC 中使用Filter
,应该怎么做?
有的童鞋在上一节的 Web 应用中可能发现了,如果注册时输入中文会导致乱码,因为 Servlet 默认按非 UTF- 8 编码读取参数。为了修复这一问题,我们可以简单地使用一个 EncodingFilter,在全局范围类给 HttpServletRequest
和HttpServletResponse
强制设置为 UTF- 8 编码。
可以自己编写一个 EncodingFilter,也可以直接使用 Spring MVC 自带的一个 CharacterEncodingFilter
。配置 Filter 时,只需在web.xml
中声明即可:
<web-app>
<filter>
<filter-name>encodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<init-param>
<param-name>forceEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>encodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
...
</web-app>
因为这种 Filter 和我们业务关系不大,注意到 CharacterEncodingFilter
其实和 Spring 的 IoC 容器没有任何关系,两者均互不知晓对方的存在,因此,配置这种 Filter 十分简单。
我们再考虑这样一个问题:如果允许用户使用 Basic 模式进行用户验证,即在 HTTP 请求中添加头Authorization: Basic email:password
,这个需求如何实现?
编写一个 AuthFilter
是最简单的实现方式:
@Component
public class AuthFilter implements Filter {@Autowired
UserService userService;
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {HttpServletRequest req = (HttpServletRequest) request;
// 获取 Authorization 头:
String authHeader = req.getHeader("Authorization");
if (authHeader != null && authHeader.startsWith("Basic")) {// 从 Header 中提取 email 和 password:
String email = prefixFrom(authHeader);
String password = suffixFrom(authHeader);
// 登录:
User user = userService.signin(email, password);
// 放入 Session:
req.getSession().setAttribute(UserController.KEY_USER, user);
}
// 继续处理请求:
chain.doFilter(request, response);
}
}
现在问题来了:在 Spring 中创建的这个 AuthFilter
是一个普通 Bean,Servlet 容器并不知道,所以它不会起作用。
如果我们直接在 web.xml
中声明这个 AuthFilter
,注意到AuthFilter
的实例将由 Servlet 容器而不是 Spring 容器初始化,因此,@Autowire
根本不生效,用于登录的 UserService
成员变量永远是null
。
所以,得通过一种方式,让 Servlet 容器实例化的 Filter,间接引用 Spring 容器实例化的AuthFilter
。Spring MVC 提供了一个DelegatingFilterProxy
,专门来干这个事情:
<web-app>
<filter>
<filter-name>authFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>authFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
...
</web-app>
我们来看实现原理:
- Servlet 容器从
web.xml
中读取配置,实例化DelegatingFilterProxy
,注意命名是authFilter
; - Spring 容器通过扫描
@Component
实例化AuthFilter
。
当 DelegatingFilterProxy
生效后,它会自动查找注册在 ServletContext
上的 Spring 容器,再试图从容器中查找名为 authFilter
的 Bean,也就是我们用 @Component
声明的AuthFilter
。
DelegatingFilterProxy
将请求代理给AuthFilter
,核心代码如下:
public class DelegatingFilterProxy implements Filter {private Filter delegate;
public void doFilter(...) throws ... {if (delegate == null) {delegate = findBeanFromSpringContainer();
}
delegate.doFilter(req, resp, chain);
}
}
这就是一个代理模式的简单应用。我们画个图表示它们之间的引用关系如下:
┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
┌─────────────────────┐ ┌───────────┐ │
│ │DelegatingFilterProxy│─│─│─ ─▶│AuthFilter │
└─────────────────────┘ └───────────┘ │
│ ┌─────────────────────┐ │ │ ┌───────────┐
│ DispatcherServlet │─ ─ ─ ─▶│Controllers│ │
│ └─────────────────────┘ │ │ └───────────┘
│
│ Servlet Container │ │ Spring Container
─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘
如果在 web.xml
中配置的 Filter 名字和 Spring 容器的 Bean 的名字不一致,那么需要指定 Bean 的名字:
<filter>
<filter-name>basicAuthFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<!-- 指定 Bean 的名字 -->
<init-param>
<param-name>targetBeanName</param-name>
<param-value>authFilter</param-value>
</init-param>
</filter>
实际应用时,尽量保持名字一致,以减少不必要的配置。
要使用 Basic 模式的用户认证,我们可以使用 curl 命令测试。例如,用户登录名是 [email protected]
,口令是tomcat
,那么先构造一个使用 URL 编码的 用户名: 口令
的字符串:
tom%40example.com:tomcat
对其进行 Base64 编码,最终构造出的 Header 如下:
Authorization: Basic dG9tJTQwZXhhbXBsZS5jb206dG9tY2F0
使用如下的 curl
命令并获得响应如下:
$ curl -v -H 'Authorization: Basic dG9tJTQwZXhhbXBsZS5jb206dG9tY2F0' http://localhost:8080/profile
> GET /profile HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.64.1
> Accept: */*
> Authorization: Basic dG9tJTQwZXhhbXBsZS5jb206dG9tY2F0
>
< HTTP/1.1 200
< Set-Cookie: JSESSIONID=CE0F4BFC394816F717443397D4FEABBE; Path=/; HttpOnly
< Content-Type: text/html;charset=UTF-8
< Content-Language: en-CN
< Transfer-Encoding: chunked
< Date: Wed, 29 Apr 2020 00:15:50 GMT
<
<!doctype html>
...HTML 输出...
上述响应说明 AuthFilter
已生效。
注意
Basic 认证模式并不安全,本节只用来作为使用 Filter 的示例。
练习
使用 DelegatingFilterProxy 实现 AuthFilter。
下载练习
小结
当一个 Filter 作为 Spring 容器管理的 Bean 存在时,可以通过 DelegatingFilterProxy
间接地引用它并使其生效。