5.1.3 服务器端校验

前一节已经介绍了,当我们为图4.2所示的页面增加了客户端校验后,如果浏览者的输入不满足基本要求,将无法通过该页面提交请求——请注意这句话,说得很清楚,仅仅是不能通过该页面提交请求,那么其他用户完全可以通过其他方式提交请求(很多恶意的Cracker,并不是通过浏览器来crack某个应用,他会采用更底层的Socket通信进行crack)。

即使不采用任何高明的手段,我们只是在图4.2所示的页面中,单击Firefox浏览器的“页面另存为”菜单,将该页面的源代码保存在计算机的任何位置,然后对该页面源代码进行简单修改,取消该表单元素的输入校验绑定,并修改该表单的action属性。修改后的表单元素的代码如下:

<!-- 取消表单元素的输入校验绑定,并修改表单元素的action属性 -->
<form action="http://localhost:8888/ClientValidate/regist" method="post">
      ...
</form>

当修改了表单的action属性后,修改后的表单将会直接向该action指定的URL提交,这个URL是一个绝对地址:http://localhost:8888/ClientValidate/regist,此URL是笔者应用中处理该请求Servlet的URL。

如果在上面的输入页面中不输入任何信息,直接单击“注册”按钮,将可以把请求提交给服务器。

对于一个有经验的Cracker而言,他至少有100种方法来绕过客户端校验。因此,服务器端校验是必做的,而且必须更严格。

提示:

客户端校验的主要作用是防止正常浏览者的误输入,仅能对输入进行初步过滤;对于恶意用户的恶意行为,客户端校验将无能为力。因此,客户端校验绝不可代替服务器端校验。当然,客户端校验也绝不可少,因为Web应用的大部分浏览者都是正常的浏览者,他们的输入可能包含大量的误输入,客户端校验把这些误输入阻止在客户端,从而降低了服务器的负载。服务器端校验是请求数据进入系统之前的最后屏障。

从上面的介绍可以看出,服务器端校验是整个应用的最后防线,它阻止了非法数据进入系统,对于系统的安全性、完整性,承载着不可替代的作用。

下面是处理该请求的Servlet代码,该Servlet代码中增加了服务器端校验代码。

程序清单:codes\05\5.1\Validate\WEB-INF\src\org\crazyit\struts2\web\RegistServlet.java

@WebServlet(urlPatterns="/regist")
public class RegistServlet extends HttpServlet
{
    // 处理用户请求的service方法
    public void service(HttpServletRequest request ,
          HttpServletResponse response)
          throws IOException, ServletException
    {
          // 设置解析请求参数所用的解码集
          request.setCharacterEncoding("GBK");
          // 从HttpServletRequest中取出4个请求参数
          String name = request.getParameter("username");
          String pass = request.getParameter("pass");
          String strAge = request.getParameter("age");
          String strBirth = request.getParameter("birth");
          // 错误字符串
          String errStr = "";
          // 校验用户名为空的情形
          if (name == null || name.trim().equals(""))
          {
                errStr = "您必须输入用户名!";
          }
          // 要求用户名必须是字母和数字,且长度必须在4到25之间
          else if (!Pattern.matches("\\w{4,25}", name.trim()))
          {
                errStr += "<br />您输入的用户名必须是字母和数字,且长度必须在4到25之间!";
          }
          // 校验密码为空的情形
          if (pass == null
                || pass.trim().equals(""))
          {
                errStr += "<br />您必须输入密码!";
          }
          // 要求密码必须是字母和数字,且长度必须在4到25之间
          else if (!Pattern.matches("\\w{4,25}", pass.trim()))
          {
                errStr += "<br />您输入的密码必须是字母和数字,且长度必须在4到25之间!";
          }
          int age = 0;
          try
          {
                // 将用户输入的年龄解析成一个整数
                age = Integer.parseInt(strAge);
          // 如果年龄大于150或者小于0
          if (age > 150 || age <= 0)
          {
                errStr += "<br />您输入的年龄必须是一个有效的年龄!";
          }
      }
      // 如果将用户输入的年龄转换成整数出现异常,表明输入不合法
      catch (Exception e)
      {
          errStr += "<br />您输入的年龄必须是整数!";
      }
      // 构造一个日期格式器
      SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-DD");
      Date birth = null;
      try
      {
          // 将用户输入的生日字符串转换成一个日期变量
          birth = sdf.parse(strBirth);
          // 判断用户输入的日期必须在有效的时间段内
          if (birth.after(sdf.parse("2050-02-21"))
                || birth.before(sdf.parse("1900-01-01")))
          {
                errStr += "<br />您输入的生日必须在一个有效的时间段内";
          }
      }
      // 如果将用户输入的生日字符串转换成日期出现异常,表明输入不合法
      catch (Exception e)
      {
          errStr += "<br />您输入的生日必须是yyyy-MM-DD格式!";
      }
      // 如果错误字符串为空,表明没有出现任何异常
      if (errStr.equals(""))
      {
          // 将类型转换后的值封装成UserBean值对象
          UserBean user = new UserBean(name , pass , age , birth);
          // 用Servlet直接输出
          response.setContentType("text/html;charset=GBK");
          // 获得页面输出流
          PrintWriter out = response.getWriter();
          out.println("<html>");
          out.println("<head>");
          // 输出页面标题
          out.println("  <title>类型转换页面</title>");
          out.println("</head>");
          out.println("<body>");
          out.println("<h1>类型转换页面</h1>");
          // 下面输出值对象user的4个属性值
          out.println("用户的用户名:" + user.getName() + "<br />");
          out.println("用户的密码:" + user.getPass() + "<br />");
          out.println("用户的年龄:" + user.getAge() + "<br />");
          out.println("用户的生日:" + user.getBirth() + "<br />");
          out.println("</body>");
          out.println("</html>");
      }
      // 否则,校验失败
      else
      {
          // 将错误字符串设置成一个HttpServletRequest属性
          request.setAttribute("error" , errStr);
          // 将请求转发到登录页面
          request.getRequestDispatcher("/regist.jsp")
                    .forward(request ,        response);
        }
    }
}

通过上面的粗体字代码为Servlet增加了校验规则后,系统的输入校验才算真正完整。此时,即使恶意浏览者绕过客户端校验,直接向该Servlet发送请求,该Servlet一样可以应付。

如果试图绕过客户端校验输入,直接向该Servlet发送非法的请求参数,该Servlet会检测到这些非法数据,并将请求转向到系统的/regist.jsp页面。如果向该Servlet提交非法数据,将看到如图5.3所示的页面。

图5.3 服务器端校验失败的页面

提示:

上面的登录页面可以显示服务器端校验失败的提示,因为上面的 regist.jsp 页面使用了表达式语言来输出HttpServletRequest请求中的error属性值。

在上面的输入校验代码中,有一些输入校验是通过类型转换实现的,也就是说,输入校验和类型转换的关系是非常紧密的。