什么是过滤器
过滤器是 Servlet 的高级特性之一, 也别把它想得那么高深, 只不过是实现 Filter 接口的 Java 类罢了!
首先, 我们来看看过滤器究竟 web 容器的哪处:
从上面的图我们可以发现, 当浏览器发送请求给服务器的时候, 先执行过滤器, 然后才访问 Web 的资源服务器响应 Response, 从 Web 资源抵达浏览器之前, 也会途径过滤器
我们很容易发现, 过滤器可以比喻成一张滤网我们想想现实中的滤网可以做什么: 在泡茶的时候, 过滤掉茶叶那滤网是怎么过滤茶叶的呢? 规定大小的网孔, 只要网孔比茶叶小, 就可以实现过滤了!
引申在 Web 容器中, 过滤器可以做: 过滤一些敏感的字符串规定不能出现敏感字符串避免中文乱码规定 Web 资源都使用 UTF-8 编码权限验证规定只有带 Session 或 Cookie 的浏览器, 才能访问 web 资源等等等, 过滤器的作用非常大, 只要发挥想象就可以有意想不到的效果
也就是说: 当需要限制用户访问某些资源时在处理请求时提前处理某些资源服务器响应的内容对其进行处理再返回我们就是用过滤器来完成的!
为什么需要用到过滤器
直接举例子来说明吧:
没有过滤器解决中文乱码问题
如果我没有用到过滤器: 浏览器通过 http 请求发送数据给 Servlet, 如果存在中文, 就必须指定编码, 否则就会乱码!
jsp 页面提交中文数据给 Servlet 处理
- <form action="${pageContext.request.contextPath}/Demo1" method="post">
- <input type="text" name="username">
- <input type="submit" value="提交">
- </form>
Servlet 没有指定编码的情况下, 获取得到的是乱码
Servlet 中如何解决中文乱码问题, 我的其他博文中有: blog.csdn.net/hon_3y/arti
也就是说: 如果我每次接受客户端带过来的中文数据, 在 Serlvet 中都要设定编码这样代码的重复率太高了!!!!
有过滤器解决中文乱码问题
有过滤器的情况就不一样了: 只要我在过滤器中指定了编码, 可以使全站的 Web 资源都是使用该编码, 并且重用性是非常理想的!
过滤器 API
只要 Java 类实现了 Filter 接口就可以称为过滤器! Filter 接口的方法也十分简单:
其中 init()和 destory()方法就不用多说了, 他俩跟 Servlet 是一样的只有在 Web 服务器加载和销毁的时候被执行, 只会被执行一次!
值得注意的是 doFilter()方法,** 它有三个参数(ServletRequest,ServletResponse,FilterChain),** 从前两个参数我们可以发现: 过滤器可以完成任何协议的过滤操作!
那 FilterChain 是什么东西呢? 我们看看:
FilterChain 是一个接口, 里面又定义了 doFilter()方法这究竟是怎么回事啊??????
我们可以这样理解: 过滤器不单单只有一个, 那么我们怎么管理这些过滤器呢? 在 Java 中就使用了链式结构把所有的过滤器都放在 FilterChain 里边, 如果符合条件, 就执行下一个过滤器(如果没有过滤器了, 就执行目标资源)
上面的话好像有点拗口, 我们可以想象生活的例子: 现在我想在茶杯上能过滤出石头和茶叶出来石头在一层, 茶叶在一层所以茶杯的过滤装置应该有两层滤网这个过滤装置就是 FilterChain, 过滤石头的滤网和过滤茶叶的滤网就是 Filter 在石头滤网中, 茶叶是属于下一层的, 就把茶叶放行, 让茶叶的滤网过滤茶叶过滤完茶叶了, 剩下的就是茶(茶就可以比喻成我们的目标资源)
快速入门
写一个简单的过滤器
实现 Filter 接口的 Java 类就被称作为过滤器
- public class FilterDemo1 implements Filter {
- public void destroy() {
- }
- public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {
- // 执行这一句, 说明放行(让下一个过滤器执行, 如果没有过滤器了, 就执行执行目标资源)
- chain.doFilter(req, resp);
- }
- public void init(FilterConfig config) throws ServletException {
- }
- }
filter 部署
过滤器和 Servlet 是一样的, 需要部署到 Web 服务器上的
第一种方式: 在 web.xml 文件中配置
filter
<filter > 用于注册过滤器
- <filter>
- <filter-name>FilterDemo1</filter-name>
- <filter-class>FilterDemo1</filter-class>
- <init-param>
- <param-name>word_file</param-name>
- <param-value>/WEB-INF/word.txt</param-value>
- </init-param>
- </filter>
<filter-name > 用于为过滤器指定一个名字, 该元素的内容不能为空
<filter-class > 元素用于指定过滤器的完整的限定类名
<init-param > 元素用于为过滤器指定初始化参数, 它的子元素指定参数的名字,<param-value > 指定参数的值在过滤器中, 可以使用 FilterConfig 接口对象来访问初始化参数
filter-mapping
<filter-mapping > 元素用于设置一个 Filter 所负责拦截的资源
一个 Filter 拦截的资源可通过两种方式来指定: Servlet 名称和资源访问的请求路径
- <filter-mapping>
- <filter-name>FilterDemo1</filter-name>
- <url-pattern>/*</url-pattern>
- </filter-mapping>
<filter-name > 子元素用于设置 filter 的注册名称该值必须是在元素中声明过的过滤器的名字
<url-pattern > 设置 filter 所拦截的请求路径(过滤器关联的 URL 样式)
<servlet-name > 指定过滤器所拦截的 Servlet 名称
<dispatcher > 指定过滤器所拦截的资源被 Servlet 容器调用的方式, 可以是 REQUEST,INCLUDE,FORWARD 和 ERROR 之一, 默认 REQUEST 用户可以设置多个 < dispatcher> 子元素用来指定 Filter 对资源的多种调用方式进行拦截
dispatcher
子元素可以设置的值及其意义:
REQUEST: 当用户直接访问页面时, Web 容器将会调用过滤器如果目标资源是通过 RequestDispatcher 的 include()或 forward()方法访问时, 那么该过滤器就不会被调用
INCLUDE: 如果目标资源是通过 RequestDispatcher 的 include()方法访问时, 那么该过滤器将被调用除此之外, 该过滤器不会被调用
FORWARD: 如果目标资源是通过 RequestDispatcher 的 forward()方法访问时, 那么该过滤器将被调用, 除此之外, 该过滤器不会被调用
ERROR: 如果目标资源是通过声明式异常处理机制调用时, 那么该过滤器将被调用除此之外, 过滤器不会被调用
第二种方式: 通过注解配置
@WebFilter(filterName = "FilterDemo1", urlPatterns = "/*")
上面的配置是 /*, 所有的 Web 资源都需要途径过滤器
如果想要部分的 Web 资源进行过滤器过滤则需要指定 Web 资源的名称即可!
过滤器的执行顺序
上面已经说过了, 过滤器的 doFilter()方法是极其重要的, FilterChain 接口是代表着所有的 Filter,FilterChain 中的 doFilter()方法决定着是否放行下一个过滤器执行(如果没有过滤器了, 就执行目标资源)
测试一
首先在过滤器的 doFilter()中输出一句话, 并且调用 chain 对象的 doFilter()方法
- public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {
- System.out.println("我是过滤器 1");
- // 执行这一句, 说明放行(让下一个过滤器执行, 或者执行目标资源)
- chain.doFilter(req, resp);
- }
我们来访问一下 test.jsp 页面:
我们发现 test.jsp(我们的目标资源)成功访问到了, 并且在服务器上也打印了字符串!
测试二
我们来试试把
chain.doFilter(req, resp);
这段代码注释了看看!
test.jsp 页面并没有任何的输出(也就是说, 并没有访问到 jsp 页面)
测试三
直接看下面的代码我们已经知道了准备放行会被打印在控制台上和 test.jsp 页面也能被访问得到, 但放行完成会不会打印在控制台上呢?
- public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {
- System.out.println("准备放行");
- // 执行这一句, 说明放行(让下一个过滤器执行, 或者执行目标资源)
- chain.doFilter(req, resp);
- System.out.println("放行完成");
- }
答案也非常简单, 肯定会打印在控制台上的我们来看看:
注意, 它的完整流程顺序是这样的: 客户端发送 http 请求到 Web 服务器上, Web 服务器执行过滤器, 执行到准备放行时, 就把字符串输出到控制台上, 接着执行 doFilter()方法, Web 服务器发现没有过滤器了, 就执行目标资源 (也就是 test.jsp) 目标资源执行完后, 回到过滤器上, 继续执行代码, 然后输出放行完成
测试四
我们再多加一个过滤器, 看看执行顺序
过滤器 1
- System.out.println("过滤器 1 开始执行");
- // 执行这一句, 说明放行(让下一个过滤器执行, 或者执行目标资源)
- chain.doFilter(req, resp);
- System.out.println("过滤器 1 开始完毕");
过滤器 2
- System.out.println("过滤器 2 开始执行");
- chain.doFilter(req, resp);
- System.out.println("过滤器 2 开始完毕");
- Servlet
- System.out.println("我是 Servlet1");
当我们访问 Servlet1 的时候, 看看控制台会出现什么:
执行顺序是这样的: 先执行 FilterDemo1, 放行, 执行 FilterDemo2, 放行, 执行 Servlet1,Servlet1 执行完回到 FilterDemo2 上, FilterDemo2 执行完毕后, 回到 FilterDemo1 上
注意: 过滤器之间的执行顺序看在 web.xml 文件中 mapping 的先后顺序的, 如果放在前面就先执行, 放在后面就后执行! 如果是通过注解的方式配置, 就比较 urlPatterns 的字符串优先级
Filter 简单应用
filter 的三种典型应用:
1 可以在 filter 中根据条件决定是否调用 chain.doFilter(request, response)方法, 即是否让目标资源执行
2 在让目标资源执行之前, 可以对 request\response 作预处理, 再让目标资源执行
3 在目标资源执行之后, 可以捕获目标资源的执行结果, 从而实现一些特殊的功能
禁止浏览器缓存所有动态页面
- public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException,
- IOException {
- // 让 Web 资源不缓存, 很简单, 设置 http 中 response 的请求头即可了!
- // 我们使用的是 http 协议, ServletResponse 并没有能够设置请求头的方法, 所以要强转成 HttpServletRequest
- // 一般我们写 Filter 都会把他俩强转成 Http 类型的
- HttpServletRequest request = (HttpServletRequest) req;
- HttpServletResponse response = (HttpServletResponse) resp;
- response.setDateHeader("Expires", -1);
- response.setHeader("Cache-Control", "no-cache");
- response.setHeader("Pragma", "no-cache");
- // 放行目标资源的 response 已经设置成不缓存的了
- chain.doFilter(request, response);
- }
没有过滤之前, 响应头是这样的:
过滤之后, 响应头是这样的:
实现自动登陆
开发实体集合模拟数据库 Dao
实体:
- private String username;
- private String password;
- public User() {}
- public User(String username, String password) {
- this.username = username;
- this.password = password;
- }
- // 各种 setter 和 getter
集合模拟数据库
- public class UserDB {
- private static List < User > users = new ArrayList < >();
- static {
- users.add(new User("aaa", "123"));
- users.add(new User("bbb", "123"));
- users.add(new User("ccc", "123"));
- }
- public static List < User > getUsers() {
- return users;
- }
- public static void setUsers(List < User > users) {
- UserDB.users = users;
- }
- }
开发 dao
- public User find(String username, String password) {
- List < User > userList = UserDB.getUsers();
- // 遍历 List 集合, 看看有没有对应的 username 和 password
- for (User user: userList) {
- if (user.getUsername().equals(username) && user.getPassword().equals(password)) {
- return user;
- }
- }
- return null;
- }
登陆界面
<form action="${pageContext.request.contextPath}/LoginServlet">
用户名 < input type="text" name="username">
<br>
密码 < input type="password" name="password">
<br>
<input type="radio" name="time" value="10">10 分钟
<input type="radio" name="time" value="30">30 分钟
<input type="radio" name="time" value="60">1 小时
- <br>
- <input type="submit" value="登陆">
- </form>
处理登陆的 Servlet
- // 得到客户端发送过来的数据
- String username = request.getParameter("username");
- String password = request.getParameter("password");
- UserDao userDao = new UserDao();
- User user = userDao.find(username, password);
- if (user == null) {
- request.setAttribute("message", "用户名或密码是错的!");
- request.getRequestDispatcher("/message.jsp").forward(request, response);
- }
- // 如果不是为空, 那么在 session 中保存一个属性
- request.getSession().setAttribute("user", user);
- request.setAttribute("message", "恭喜你, 已经登陆了!");
- // 如果想要用户关闭了浏览器, 还能登陆, 就必须要用到 Cookie 技术了
- Cookie cookie = new Cookie("autoLogin", user.getUsername() + "." + user.getPassword());
- // 设置 Cookie 的最大声明周期为用户指定的
- cookie.setMaxAge(Integer.parseInt(request.getParameter("time")) * 60);
- // 把 Cookie 返回给浏览器
- response.addCookie(cookie);
- // 跳转到提示页面
- request.getRequestDispatcher("/message.jsp").forward(request, response);
过滤器
- HttpServletResponse response = (HttpServletResponse) resp;
- HttpServletRequest request = (HttpServletRequest) req;
- // 如果用户没有关闭浏览器, 就不需要 Cookie 做拼接登陆了
- if (request.getSession().getAttribute("user") != null) {
- chain.doFilter(request, response);
- return;
- }
- // 用户关闭了浏览器, session 的值就获取不到了所以要通过 Cookie 来自动登陆
- Cookie[] cookies = request.getCookies();
- String value = null;
- for (int i = 0; cookies != null && i < cookies.length; i++) {
- if (cookies[i].getName().equals("autoLogin")) {
- value = cookies[i].getValue();
- }
- }
- // 得到 Cookie 的用户名和密码
- if (value != null) {
- String username = value.split("\\.")[0];
- String password = value.split("\\.")[1];
- UserDao userDao = new UserDao();
- User user = userDao.find(username, password);
- if (user != null) {
- request.getSession().setAttribute("user", user);
- }
- }
- chain.doFilter(request, response);
效果:
改良
我们直接把用户名和密码都放在了 Cookie 中, 这是明文的懂点编程的人就会知道你的账号了
于是乎, 我们要对密码进行加密!
Cookie cookie = new Cookie("autoLogin", user.getUsername() + "." + md5.md5(user.getPassword()));
在过滤器中, 加密后的密码就不是数据库中的密码的所以, 我们得在 Dao 添加一个功能根据用户名, 找到用户
- public User find(String username) {
- List < User > userList = UserDB.getUsers();
- // 遍历 List 集合, 看看有没有对应的 username 和 password
- for (User user: userList) {
- if (user.getUsername().equals(username)) {
- return user;
- }
- }
- return null;
- }
在过滤器中, 比较 Cookie 带过来的 md5 密码和在数据库中获得的密码 (也经过 md5) 是否相同
- // 得到 Cookie 的用户名和密码
- if (value != null) {
- String username = value.split("\\.")[0];
- String password = value.split("\\.")[1];
- // 在 Cookie 拿到的密码是 md5 加密过的, 不能直接与数据库中的密码比较
- UserDao userDao = new UserDao();
- User user = userDao.find(username);
- // 通过用户名获得用户信息, 得到用户的密码, 用户的密码也 md5 一把
- String dbPassword = md5.md5(user.getPassword());
- // 如果两个密码匹配了, 就是正确的密码了
- if (password.equals(dbPassword)) {
- request.getSession().setAttribute("user", user);
- }
- }
如果文章有错的地方欢迎指正, 大家互相交流习惯在微信看技术文章的同学, 可以关注微信公众号: Java3y
来源: https://juejin.im/post/5a7be1fbf265da4e8263377f