共计 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
是最简单的实现方式:
public class AuthFilter implements Filter { | |
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 | |
< | |
...HTML 输出... |
上述响应说明 AuthFilter
已生效。
注意
Basic 认证模式并不安全,本节只用来作为使用 Filter 的示例。
练习
使用 DelegatingFilterProxy 实现 AuthFilter。
下载练习
小结
当一个 Filter 作为 Spring 容器管理的 Bean 存在时,可以通过 DelegatingFilterProxy
间接地引用它并使其生效。
