1. 引言

我们可以通过使用前面的技术,做出一些简单的登陆注册以及配合数据库实现对数据增删改查的Demo,程序是基本运行起来了,但是却存在着一个重大的安全问题,那就登陆权限验证,一般来说登陆的正确流程是这样的:用户在客户端发出请求 -> 后台判断是否登录 -> 是则不限制,否则 跳转回登录页面,判断是否登录和我们前面所学习的 Header中获取referer再判断达从而到防盗链的效果有相似的感觉,就是起一个判断过滤的样子,而Filter则是一个更好的解决这样问题的技术,当然强大的功能不止这一点,下面我们就好好来说一说!

2. 过滤器概述

过滤器,顾名思义就是起到过滤筛选作用的一种事物,只不过相较于现实生活中的过滤器,这里的过滤器过滤的对象是客户端访问的web资源,也可以理解为一种预处理手段,对资源进行拦截后,将其中我们认为的杂质(用户自己定义的)过滤,符合条件的放行,不符合的则拦截下来

当然,过滤器既可以拦截request,也可以拦截返回的response,我们来看一张图
filter.png

3. 第一个过滤器程序

过滤器的本质就是一个实现了 Filter 接口的 Java 类
我们先自己创建一个类,实现Filter接口(javax.servlet),重写其中的所有方法

  1. @WebFilter("/*")
  2. public class FilterDemo1 implements Filter {
  3. public void destroy() {
  4. }
  5. public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {
  6. //放行代码
  7. chain.doFilter(req, resp);
  8. }
  9. public void init(FilterConfig config) throws ServletException {
  10. }
  11. }

我们先不探究其中的方法,我们先看一下如何配置filter

3.1 第一种 Filter 配置:web.xml配置

  1. <filter>
  2. <filter-name>filterDemo1</filter-name>
  3. <filter-class>package cn.ideal.web.filter.FilterDemo1</filter-class>
  4. </filter>
  5. <filter-mapping>
  6. <filter-name>filterDemo1</filter-name>
  7. <!-- 拦截路径 -->
  8. <url-pattern>/*</url-pattern>
  9. </filter-mapping>

filter

<filter-name></filter-name> :指定filter名字
<filter-class></filter-class> :指定filter全类名(带包名)

filter-mapping

<filter-name></filter-name> :这里的标签是为了与上面filter中的名字对应,从而指向到对应的文件中
<url-pattern></url-pattern>设置filter所拦截的路径 ※ 这里决定了什么样的资源会被过滤器拦截处理

拦截路径设置

格式 解释
/test.jsp 只有访问test.jsp这个资源的时候才会执行过滤器
/test/* 访问test下所有资源你的时候,执行过滤器
*.jsp 所有jsp格式的资源被访问的时候,执行过滤器
/* 任意资源被访问,均执行过滤器

由于过滤器内设置的是比较通用的一些设置,所以一般来说使用 /* 这种格式,不过也可以根据需求情况选择

拦截方式配置:dispatcher

拦截方式配置也就是资源被访问的形式,有这么几个属性

  • REQUEST:默认值,浏览器直接请求资源
  • FORWARD:转发访问资源 : RequestDispatcher.forward();
  • INCLUDE:包含访问资源 : RequestDispatcher.include();
  • ERROR:错误跳转资源 : 被声明式异常处理机制调用的时候

补充:声明式异常处理即:在web.xml中通过配置来确定不同的异常类型将如何被处理,最后跳转到哪个页面,也就是我们常常看到的一些404错误页面

  1. <error-page>
  2. <!--异常的类-->
  3. <exception-type>xxx</exception-type>
  4. <!--异常发生时跳转的页面-->
  5. <location>xxx</location>
  6. </error-page>

3.2 第一种 Filter 配置:使用注解配置

与servlet相似的配置 ,我们可以指定它的名字和拦截路径

  1. @WebFilter("filterName="FilterDemo1",urlPatters="/*")

但是直接在类上声明注解,显然那我们是不需要指定其名字的,而通过查看源码又可以知道,urlPatters又可以被value指定,而value又可以省略,所以我们可以简写为

  1. @WebFilter("/*")

若想在filter注解中配置dispatcher,我们需要设置dispatcherTypes属性

  1. @WebFilter(value = "/*",dispatcherTypes ={DispatcherType.FORWARD,DispatcherType.FORWARD} )

4. 过滤器的生命周期

讲完了配置,下面我们就回归主题来说一说过滤器的生命周期,也就是上面实现接口而重写的那些方法们
首先是 init(FilterConfig config) 方法和 void destroy() 方法,Servlet也有这两个方法,两者分别在服务器启动和关闭的时候被创建以及销毁,两者均执行一次,用于加载以及释放资源
其实就这两个方法来说在Servlet的基础上还是很好理解的
再者就是我们过滤器的核心方法了:

  1. void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)

doFilter方法就是我们真正进行拦截的方法,通过前两个参数我们可以知道,不论是Request亦或是Respone我们都可以对其进行过滤操作,那么第三个参数是什么意思呢?
我们打开FilterChain的源码

  1. public interface FilterChain {
  2. void doFilter(ServletRequest var1, ServletResponse var2) throws IOException, ServletException;
  3. }

嗯!FilterChain是一个接口,接口内也定义了一个doFilter方法,它存在的意义是什呢?
这是一种链式结构,我们在这里称作过滤器链,其作用就是为了配置多个过滤器,多个过滤器下的执行流程是这样的
Filter链.png
那么,多个过滤器谁前谁后呢?这还与我们前面的配置有关

  • 注解配置:按照类名字符串比较,值小的先执行
    • Eg:AFilterDemo 优先于 BFilterDemo
  • web.xml配置:<filter-mapping>中谁在上面,谁优先执行

过滤器的简单执行流程

  • 执行过滤器
  • 执行放行后的资源,可能是下一个过滤器,也可能是web资源(JSP/Servlet)
  • 执行过滤器放行代码chain.doFilter(req, resp);下边的代码

5. Filter的应用

5.1 登录权限验证

我们前面的的知识已经能简单的满足我们对于登录以及简单注册的实现,但是如果我们知道地址,直接通过url访问一些 资源,很显然这是很不合理的,所以我们需要对登录状态进行验证,未登录则转发到的登录界面,登录则可以依据登录状态自由访问一些页面
我们写一个简单的模拟程序,为了可读性,以及篇幅问题,我们省略数据库连接的部分,采用固定的密码
这是index.jsp页面,也就是需要登录后才能放开访问权限的页面

  1. <%@ page contentType="text/html;charset=UTF-8" language="java" %>
  2. <html>
  3. <head>
  4. <title>$Title$</title>
  5. </head>
  6. <body>
  7. <h1>这是首页,只有登录后才能查看</h1>
  8. </body>
  9. </html>

这是login.jsp页面,也就是登录页面,非常简单

  1. <%@ page contentType="text/html;charset=UTF-8" language="java" %>
  2. <html>
  3. <head>
  4. <title>Title</title>
  5. </head>
  6. <body>
  7. <form action="/web-test/loginServlet" method="post">
  8. <table>
  9. <tr>
  10. <td>用户名:</td>
  11. <td><input type="text" name="username"></td>
  12. </tr>
  13. <tr>
  14. <td>密码:</td>
  15. <td><input type="password" name="password"></td>
  16. </tr>
  17. <tr>
  18. <td><input type="submit" value="登录"></td>
  19. </tr>
  20. </table>
  21. </form>
  22. </body>
  23. </html>

我们创一个domain 包,写一个User实体,补充其get、set方法

  1. package cn.ideal.domain;
  2. public class User {
  3. private String username;
  4. private String password;
  5. public String getUsername() {
  6. return username;
  7. }
  8. public void setUsername(String username) {
  9. this.username = username;
  10. }
  11. public String getPassword() {
  12. return password;
  13. }
  14. public void setPassword(String password) {
  15. this.password = password;
  16. }
  17. @Override
  18. public String toString() {
  19. return "User{" +
  20. "username='" + username + '\'' +
  21. ", password='" + password + '\'' +
  22. '}';
  23. }
  24. }

下面开始编写LoginServlet,也就是处理登录验证问题的代码

  1. package cn.ideal.web.servlet;
  2. import cn.ideal.dao.UserDao;
  3. import cn.ideal.domain.User;
  4. import javax.servlet.ServletException;
  5. import javax.servlet.annotation.WebServlet;
  6. import javax.servlet.http.HttpServlet;
  7. import javax.servlet.http.HttpServletRequest;
  8. import javax.servlet.http.HttpServletResponse;
  9. import java.io.IOException;
  10. @WebServlet("/loginServlet")
  11. public class LoginServlet extends HttpServlet {
  12. protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
  13. // 设置编码
  14. request.setCharacterEncoding("utf-8");
  15. // 获取请求参数
  16. String username = request.getParameter("username");
  17. String password = request.getParameter("password");
  18. // 封装user对象
  19. User loginUser = new User();
  20. loginUser.setUsername(username);
  21. loginUser.setPassword(password);
  22. UserDao dao = new UserDao();
  23. User user = dao.login(loginUser);
  24. if (user == null){
  25. // 登陆失败
  26. request.getRequestDispatcher("/failServlet").forward(request,response);
  27. }else{
  28. // 登录成功
  29. request.getSession().setAttribute("user",user);
  30. request.getRequestDispatcher("/index.jsp").forward(request,response);
  31. }
  32. }
  33. protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
  34. this.doPost(request,response);
  35. }
  36. }

我们根据 user 是否等于 null来判断用户名密码是否正确,那么我们你就来写一下这个返回了一个User对象的login方法
我们在dao层中创建一个UserDao类,正式一些的项目会写成接口的形式,在impl层中再写实现,为了掩饰我们简化这一步

  1. package cn.ideal.dao;
  2. import cn.ideal.domain.User;
  3. public class UserDao {
  4. public User login(User loginUser) {
  5. //定义真实用户名密码(代替数据库读取)
  6. String trueUsername = "admin";
  7. String truePassword = "admin";
  8. if (loginUser.getUsername().equals(trueUsername) && loginUser.getPassword().equals(truePassword)) {
  9. //登陆成功
  10. return loginUser;
  11. } else {
  12. return null;
  13. }
  14. }
  15. }

关键来了,这也就是我们所讲的过滤器方法,这里所需要注意的就是 登陆成功后,记得写入状态
**request.getSession().setAttribute("user",user);**

  1. package cn.ideal.web.filter;
  2. import javax.servlet.*;
  3. import javax.servlet.annotation.WebFilter;
  4. import javax.servlet.http.HttpServletRequest;
  5. import javax.servlet.http.HttpServletResponse;
  6. import java.io.IOException;
  7. @WebFilter("/*")
  8. public class LoginFilter implements Filter {
  9. public void destroy() {
  10. }
  11. public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {
  12. HttpServletRequest request = (HttpServletRequest) req;
  13. HttpServletResponse response = (HttpServletResponse) resp;
  14. // 获取资源请求路径
  15. String requestURI = request.getRequestURI();
  16. // 排除包含登录确实所需要的资源,给予放行
  17. if (requestURI.contains("/login.jsp") || requestURI.contains("/loginServlet")) {
  18. chain.doFilter(request,response);
  19. }else{
  20. // 不包含,即验证用户是否已经登录
  21. Object user = request.getSession().getAttribute("user");
  22. if (user != null){
  23. // 登陆了,放行
  24. chain.doFilter(request,response);
  25. }else{
  26. // 没有登录,跳转回登录页面
  27. request.getRequestDispatcher("/login.jsp").forward(request,response);
  28. }
  29. }
  30. }
  31. public void init(FilterConfig config) throws ServletException {
  32. }
  33. }

2. 敏感词过滤

如果我们想要对用户提交的一些信息进行过滤,在servlet中进行一些代码的编写也算一种方法,但是最为合适的还是fiter,它更加通用,下面我们使用代理模式增强request从而使用filter进行敏感词的过滤
我们就在刚才的index页面上加以修改

  1. <%@ page contentType="text/html;charset=UTF-8" language="java" %>
  2. <html>
  3. <head>
  4. <title>$Title$</title>
  5. </head>
  6. <body>
  7. <h1>这是首页,只有登录后才能查看</h1>
  8. <form action="/web-test/replaceServlet" method="post">
  9. <table>
  10. <tr>
  11. <td><input type="text" name="words"></td>
  12. </tr>
  13. <tr>
  14. <td><input type="submit" value="敏感字检测"></td>
  15. </tr>
  16. </table>
  17. </form>
  18. </body>
  19. </html>

我们把传入的参数读取进来

  1. protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
  2. request.setCharacterEncoding("UTF-8");
  3. String words = request.getParameter("words");
  4. System.out.println(words);
  5. }
  1. package cn.ideal.web.filter;
  2. import javax.servlet.*;
  3. import javax.servlet.annotation.WebFilter;
  4. import java.io.*;
  5. import java.lang.reflect.InvocationHandler;
  6. import java.lang.reflect.Method;
  7. import java.lang.reflect.Proxy;
  8. import java.util.ArrayList;
  9. import java.util.List;
  10. @WebFilter("/*")
  11. public class ReplaceFilter implements Filter {
  12. public void destroy() {
  13. }
  14. public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {
  15. // 创建代理对象,增强getParameter
  16. ServletRequest proxy_req = (ServletRequest) Proxy.newProxyInstance(req.getClass().getClassLoader(), req.getClass().getInterfaces(), new InvocationHandler() {
  17. @Override
  18. public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  19. // 判断是不是getParameter方法
  20. if (method.getName().equals("getParameter")){
  21. // 获取返回值
  22. String value = (String)method.invoke(req, args);
  23. if (value != null){
  24. for (String s : list){
  25. if (value.contains(s)){
  26. value = value.replaceAll(s,"***");
  27. }
  28. }
  29. }
  30. return value;
  31. }
  32. return method.invoke(req,args);
  33. }
  34. });
  35. chain.doFilter(proxy_req, resp);
  36. }
  37. private List<String> list = new ArrayList<String>();
  38. public void init(FilterConfig config) throws ServletException {
  39. try {
  40. // 获取文件真实路径
  41. ServletContext servletContext = config.getServletContext();
  42. String realPath = servletContext.getRealPath("/WEB-INF/classes/replace.txt");
  43. // 读取文件
  44. BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(realPath),"UTF-8"));
  45. // 将每一行数据添加到list中
  46. String line = null;
  47. while((line = br.readLine())!=null){
  48. list.add(line);
  49. }
  50. } catch (FileNotFoundException e) {
  51. e.printStackTrace();
  52. } catch (IOException e) {
  53. e.printStackTrace();
  54. }
  55. }
  56. }