集成Filter

在Spring MVC中,DispatcherServlet只需要固定配置到web.xml中,剩下的工作主要是专注于编写Controller。

但是,在Servlet规范中,我们还可以使用Filter。如果要在Spring MVC中使用Filter,应该怎么做?

有的童鞋在上一节的Web应用中可能发现了,如果注册时输入中文会导致乱码,因为Servlet默认按非UTF-8编码读取参数。为了修复这一问题,我们可以简单地使用一个EncodingFilter,在全局范围类给HttpServletRequestHttpServletResponse强制设置为UTF-8编码。

可以自己编写一个EncodingFilter,也可以直接使用Spring MVC自带的一个CharacterEncodingFilter。配置Filter时,只需在web.xml中声明即可:

  1. <web-app>
  2. <filter>
  3. <filter-name>encodingFilter</filter-name>
  4. <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
  5. <init-param>
  6. <param-name>encoding</param-name>
  7. <param-value>UTF-8</param-value>
  8. </init-param>
  9. <init-param>
  10. <param-name>forceEncoding</param-name>
  11. <param-value>true</param-value>
  12. </init-param>
  13. </filter>
  14. <filter-mapping>
  15. <filter-name>encodingFilter</filter-name>
  16. <url-pattern>/*</url-pattern>
  17. </filter-mapping>
  18. ...
  19. </web-app>

因为这种Filter和我们业务关系不大,注意到CharacterEncodingFilter其实和Spring的IoC容器没有任何关系,两者均互不知晓对方的存在,因此,配置这种Filter十分简单。

我们再考虑这样一个问题:如果允许用户使用Basic模式进行用户验证,即在HTTP请求中添加头Authorization: Basic email:password,这个需求如何实现?

编写一个AuthFilter是最简单的实现方式:

  1. @Component
  2. public class AuthFilter implements Filter {
  3. @Autowired
  4. UserService userService;
  5. public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
  6. throws IOException, ServletException {
  7. HttpServletRequest req = (HttpServletRequest) request;
  8. // 获取Authorization头:
  9. String authHeader = req.getHeader("Authorization");
  10. if (authHeader != null && authHeader.startsWith("Basic ")) {
  11. // 从Header中提取email和password:
  12. String email = prefixFrom(authHeader);
  13. String password = suffixFrom(authHeader);
  14. // 登录:
  15. User user = userService.signin(email, password);
  16. // 放入Session:
  17. req.getSession().setAttribute(UserController.KEY_USER, user);
  18. }
  19. // 继续处理请求:
  20. chain.doFilter(request, response);
  21. }
  22. }

现在问题来了:在Spring中创建的这个AuthFilter是一个普通Bean,Servlet容器并不知道,所以它不会起作用。

如果我们直接在web.xml中声明这个AuthFilter,注意到AuthFilter的实例将由Servlet容器而不是Spring容器初始化,因此,@Autowire根本不生效,用于登录的UserService成员变量永远是null

所以,得通过一种方式,让Servlet容器实例化的Filter,间接引用Spring容器实例化的AuthFilter。Spring MVC提供了一个DelegatingFilterProxy,专门来干这个事情:

  1. <web-app>
  2. <filter>
  3. <filter-name>authFilter</filter-name>
  4. <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
  5. </filter>
  6. <filter-mapping>
  7. <filter-name>authFilter</filter-name>
  8. <url-pattern>/*</url-pattern>
  9. </filter-mapping>
  10. ...
  11. </web-app>

我们来看实现原理:

  1. Servlet容器从web.xml中读取配置,实例化DelegatingFilterProxy,注意命名是authFilter
  2. Spring容器通过扫描@Component实例化AuthFilter

DelegatingFilterProxy生效后,它会自动查找注册在ServletContext上的Spring容器,再试图从容器中查找名为authFilter的Bean,也就是我们用@Component声明的AuthFilter

DelegatingFilterProxy将请求代理给AuthFilter,核心代码如下:

  1. public class DelegatingFilterProxy implements Filter {
  2. private Filter delegate;
  3. public void doFilter(...) throws ... {
  4. if (delegate == null) {
  5. delegate = findBeanFromSpringContainer();
  6. }
  7. delegate.doFilter(req, resp, chain);
  8. }
  9. }

这就是一个代理模式的简单应用。我们画个图表示它们之间的引用关系如下:

  1. ┌─────────────────────┐ ┌───────────┐
  2. DelegatingFilterProxy│─│─│─ ─>│AuthFilter
  3. └─────────────────────┘ └───────────┘
  4. ┌─────────────────────┐ ┌───────────┐
  5. DispatcherServlet │─ ─>│Controllers
  6. └─────────────────────┘ └───────────┘
  7. Servlet Container Spring Container

如果在web.xml中配置的Filter名字和Spring容器的Bean的名字不一致,那么需要指定Bean的名字:

  1. <filter>
  2. <filter-name>basicAuthFilter</filter-name>
  3. <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
  4. <!-- 指定Bean的名字 -->
  5. <init-param>
  6. <param-name>targetBeanName</param-name>
  7. <param-value>authFilter</param-value>
  8. </init-param>
  9. </filter>

实际应用时,尽量保持名字一致,以减少不必要的配置。

要使用Basic模式的用户认证,我们可以使用curl命令测试。例如,用户登录名是tom@example.com,口令是tomcat,那么先构造一个使用URL编码的用户名:口令的字符串:

  1. tom%40example.com:tomcat

对其进行Base64编码,最终构造出的Header如下:

  1. Authorization: Basic dG9tJTQwZXhhbXBsZS5jb206dG9tY2F0

使用如下的curl命令并获得响应如下:

  1. $ curl -v -H 'Authorization: Basic dG9tJTQwZXhhbXBsZS5jb206dG9tY2F0' http://localhost:8080/profile
  2. > GET /profile HTTP/1.1
  3. > Host: localhost:8080
  4. > User-Agent: curl/7.64.1
  5. > Accept: */*
  6. > Authorization: Basic dG9tJTQwZXhhbXBsZS5jb206dG9tY2F0
  7. >
  8. < HTTP/1.1 200
  9. < Set-Cookie: JSESSIONID=CE0F4BFC394816F717443397D4FEABBE; Path=/; HttpOnly
  10. < Content-Type: text/html;charset=UTF-8
  11. < Content-Language: en-CN
  12. < Transfer-Encoding: chunked
  13. < Date: Wed, 29 Apr 2020 00:15:50 GMT
  14. <
  15. <!doctype html>
  16. ...HTML输出...

上述响应说明AuthFilter已生效。

注意:Basic认证模式并不安全,本节只用来作为使用Filter的示例。

练习

集成Filter - 图1下载练习:使用DelegatingFilterProxy实现AuthFilter (推荐使用IDE练习插件快速下载)

小结

当一个Filter作为Spring容器管理的Bean存在时,可以通过DelegatingFilterProxy间接地引用它并使其生效。

读后有收获可以支付宝请作者喝咖啡,读后有疑问请加微信群讨论:

集成Filter - 图2 集成Filter - 图3