4.3 服务器端校验
前面介绍了如何添加客服端校验,但是仅有客户端验证还是不够的。攻击者还可以绕过客户端校验直接进行非法输入,这样可能会引起系统的异常,所以必须加上服务器端的验证。下面来看如何添加服务器端校验。
4.3.1 服务器端校验的重要性
在上一个示例中为注册页面添加了客户端校验,如果用户输入的信息不合法则无法提交。这时这里要注意的是,使用JavaScript增加客户端校验仅仅使得非法的数据无法提交,但是一些侵入者完全可以采用其他的方式来进行提交。下面来看如何绕过这些JavaScript校验代码?
首先可以直接把这个注册页面下载下来,然后通过删除那些JavaScript代码,再修改表单的提交地址。这样的话,就算是输入不合法的信息,客户端校验也起不了作用了,因为连JavaScript代码都被删除掉了。
通过一种如此简单的方法就可以绕过这些JavaScript校验代码。那些侵入者很可能使用更加高级的手段来绕过这些JavaScript代码,从而直接提交非法的数据。要避免这种情况就必须添加服务器端校验,服务器端校验是整个Web应用中最重要的一道防线。用户是无法直接接触到服务器端代码的,这样的话就算是客户端校验被人绕过,仍然能够通过服务器端校验来阻止用户的非法输入。服务器端校验对于系统的安全性、完整性、健壮性起到了至关重要的作用。
那是不是客户端校验根本就没有什么意义了呢?其实不是,因为并不是每个用户都有这样恶意侵入的想法。大部分的用户都是采用正常的输入,使用客户端校验能够过滤掉用户的错误操作。如果没有客户端校验,那么就算用户只是一个错误的操作,服务器端就要对其输入的信息进行处理并返回错误提示,这样会大大增加服务器端的负载。客户端校验就像是一把锁,能够防君子但是不能防小人。同样,客户端校验和服务器端校验是紧密结合的,两者缺一不可。
4.3.2 完成服务器端输入校验
现在以上面那个示例的要求来添加服务器端校验。为了能更好地观察服务器端输出的错误信息,暂时不使用客户端的校验代码。这里只是暂时地去掉客户端校验代码,这样才能更好地查看服务端校验的效果。
步骤如下。
(1)首先新建一个用户注册页。在输入页中除了输入表单外,再添加一个<s:actionerror>标签用来输出actionerror中的错误信息,代码如下所示。
<%@ page language="java" import="java.util.*" pageEncoding="gb2312"%> <%@ taglib prefix="s" uri="/struts-tags" %> <! DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html> <head> <title>注册页面</title> </head> <body> <! -- 注册表单 --> <center> <! -- 输入校验错误提示 --> <s:actionerror/> <h2>注册页面</h2> <form action="register.action" method="post"> <table> <tr> <td>用户名:</td> <td><input type="text" name="username"></td> </tr> <tr> <td>密码:</td> <td><input type="password" name="password"></td> </tr> <tr> <td>确认密码:</td> <td> <input type="password" name="repassword"></td> </tr> <tr> <td>年龄: </td> <td><input type="text" name="age"></td> </tr> <tr> <td>出生日期: </td> <td><input type="text" name="birth"></td> </tr> <tr> <td>邮箱地址:</td> <td><input type="text" name="email"></td> </tr> <tr> <td></td> <td><input type="submit" value="注册"></td> </tr> </table> </form> </center> </body> </html>
(2)新建业务控制器Action。该Action继承ActionSupport类,并实现validate方法。在validate方法中添加校验代码,如果校验出错,则addActionError方法将错误信息保存到actionError中,代码如下所示。
package net.hncu.action; import java.util.Calendar; import java.util.Date; import java.util.regex.Pattern; import com.opensymphony.xwork2.ActionSupport; public class RegisterAction extends ActionSupport{ private String username; private String password; private String repassword; private int age; private Date birth; private String email; public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getRepassword() { return repassword; } public void setRepassword(String repassword) { this.repassword = repassword; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public Date getBirth() { return birth; } public void setBirth(Date birth) { this.birth = birth; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public void validate() { //判断用户名是否输入,如果输入了,再判断格式是否正确 if(username == null —— "".equals(username)){ this.addActionError("用户名必须输入"); } else if ( ! Pattern.matches("\\w{6,20}", username.trim())) { this.addActionError("用户名必须是字母和数字,长度为6到20之间"); } //判断密码是否输入,如果输入了,再判断格式是否正确 if( password == null —— "".equals(password)){ this.addActionError("密码必须输入") ; }else if( ! Pattern.matches("\\w{6,20}", password.trim())) { this.addActionError("密码必须是字母和数字,长度为6到20之间"); } //判断确认密码是否输入,如果输入了,再判断格式是否正确 if(repassword == null —— "".equals(repassword)){ this.addActionError("确认密码必须输入") ; }else if( ! Pattern.matches("\\w{6,20}", repassword.trim())) { this.addActionError("确认密码必须是字母和数字,长度为6到20之间"); } //判断确认密码和密码是否相同 if(password ! = null && repassword ! = null && ! repassword.equals(password)){ this.addActionError("确认密码和密码必须相同"); } //判断年龄是否合法 if(age < 0 —— age >130) { this.addActionError("请输入有效的年龄"); } //判断出生日期是否合法 Calendar start = Calendar.getInstance(); Calendar end = Calendar.getInstance(); start.set(1900, 1,1); end.set(2010, 1,1); if(birth ! = null && ( birth.after(end.getTime()) —— birth.before(start.getTime()))) { this.addActionError("请输入有效的出生日期"); } //判断邮箱地址是否合法 if(email ! = null && ! "".equals(email) && email ! = "" && !Pattern.matches("[a-zA-Z][a-zA-Z0-9._-]*@( [a-zA-Z0-9-_]+\\.)+(com—gov—net—com\\.cn—edu\\.cn)", email)){ this.addActionError("请输入正确的邮箱地址"); } } public String execute() throws Exception { return SUCCESS; } }
在“struts.xml”文件中配置该Action,并定义处理结果与视图资源之间的关系,代码如下所示。
<? xml version="1.0" encoding="UTF-8" ? > <! DOCTYPE struts PUBLIC "-//Apache Software Foundation//DTD Struts Configuration 2.0//EN" "http://struts.apache.org/dtds/struts-2.0.dtd"> <struts> <package name="struts2" extends="struts-default"> <! -- 定义register的Action,其实现类为net.hncu.action.RegisterAction--> <action name="register" class="net.hncu.action.RegisterAction"> <! -- 定义处理结果与视图资源之间的关系--> <result name="success">/result.jsp</result> <result name="input">/register.jsp</result> </action> </package> </struts>
通过上面的步骤,就成功地为项目添加上了服务器端校验。下面来测试一下服务器端校验。
4.3.3 测试服务器端输入校验
现在打开用户注册页,不输入任何数据,单击“注册”按钮,页面将提示按要求进行输入的错误信息,如图4.10所示。
因为程序要求输入用户名、密码、确认密码必须输入,所以会提示要求用户进行输入。同样假如输入错误非法的出生日期。如“1000-02-03”时,单击“注册”按钮,页面将会提示要求用户输入有效的出生日期,如图4.11所示。
图4.10 提示用户进行输入
图4.11 提示输入有效的出生日期
假如输入非法的年龄,如“200”时。单击“注册”按钮,页面将会提示要求用户输入有效的年龄,如图4.12所示。
图4.12 提示输入有效的年龄
这里发现了一个问题。如果输入了非法的信息,提交后显示错误信息但是以前输入的信息全部没了。这是因为在显示页中使用的是普通的HTML表单标签,它默认不提供这种保留提交信息的功能。
4.3.4 使页面保留提交信息
如果希望表单中能够保留提交的信息,可以在表单的每个元素中添加value属性,并设置值,如下所示。
<form action="register.action" method="post"> <table> <tr> <td>用户名:</td> <td><input type="text" name="username" value="${requestScope.username }"></td> </tr> <tr> <td>密码:</td> <td><input type="password" name="password" value="${requestScope.password }"></td> </tr> <tr> <td>确认密码:</td> <td> <input type="password" name="repassword" value="${requestScope.repassword }"></td> </tr> <tr> <td>年龄: </td> <td><input type="text" name="age" value="${requestScope.age }"></td> </tr> <tr> <td>出生日期: </td> <td><input type="text" name="birth" value="${requestScope.birth }"></td> </tr> <tr> <td>邮箱地址:</td> <td><input type="text" name="email" value="${requestScope.email }"></td> </tr> <tr> <td></td> <td><input type="submit" value="注册"></td> </tr> </table> </form>
再次运行用户注册页,输入一些非法的信息。单击“注册”按钮,页面将会提示错误信息,同时以前提交的信息仍然不会消失,而是保存在表单中,如图4.13所示。
图4.13 提示错误信息
这样感觉就好多了,用户以前提交的信息能够保留下来,就不需要重复输入了。不过每个表单都要添加一个value属性还是挺麻烦的。这点Struts 2的设计者也想到了,因此在Struts 2表单的表单标签中提供了保留用户提交值的机制。下面使用Struts 2的表单来改写注册页面,程序代码如下所示。
<%@ page language="java" import="java.util.*" pageEncoding="gb2312"%> <%@ taglib prefix="s" uri="/struts-tags" %> <! DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html> <head> <title>注册页面</title> </head> <body> <! -- 注册表单 --> <center> <! -- 输入校验错误提示 --> <s:actionerror/> <h2>注册页面</h2> <s:form action="register"> <s:textfield name="username" label="用户名"></s:textfield> <s:password name="password" label="密码"></s:password> <s:password name="repassword" label="确认密码"></s:password> <s:textfield name="age" label="年龄"></s:textfield> <s:textfield name="birth" label="出生日期"></s:textfield> <s:textfield name="email" label="邮箱地址"></s:textfield> <s:submit value="注册"></s:submit> </s:form> </center> </body> </html>
运行用户注册页,输入一些非法的信息。单击“注册”按钮,页面将会提示错误信息,同样以前提交的信息会保留在表单中,不过密码框中的值不会保留,如图4.14所示。
图4.14 提示错误信息
这时发现页面上除了显示输入校验的错误信息外,还显示了类型转换的错误信息。在前面曾经提到过,如果有类型转换错误,Struts 2的表单标签会将相应的错误信息显示出来。但是不用担心,后面将对输入校验和类型转换之间的关系进行详细的介绍。
4.3.5 使用addFieldError来添加错误信息
在前面介绍了使用actionError来保存输入校验错误提示信息。actionError其实就是一个ArrayList,将错误信息保存在actionError中,其实就是保存在一个ArrayList中。前面曾讲过类型转换的错误信息是保存在fieldError中,同样输入校验的错误信息也可以通过addFieldError方法来保存到fieldError中。fieldError和actionError不同的是,fieldError采用Map结构来存储,所以都是以键值对来保存信息的。
那到底是使用fieldError来保存错误提示信息还是使用actionError好呢?这个就依据项目具体要求而定了,如果只是希望在页面中单纯地显示错误提示信息,可以使用actionError来保存错误提示信息;如果希望在相应的文本框中显示错误提示信息,则需要使用fieldError来保存错误提示信息。
下面来看如何将错误提示信息保存到fieldError中。首先可以使用addFieldError方法来替代addActionError方法,从而将错误提示信息保存到fieldError中。其中addFieldError方法中包含两个参数,第一个参数用来输入参数名(也可以说是Action中属性名或者是表单元素中的name属性值),第二个参数用来输入校验错误提示信息,修改代码如下所示。
public void validate() { //判断用户名是否输入,如果输入了,再判断格式是否正确 if(username == null —— "".equals(username.trim())){ this.addFieldError("username", "用户名必须输入"); } else if ( ! Pattern.matches("\\w{6,20}", username.trim())) { this.addFieldError("username", "用户名必须是字母和数字,长度为6到20之间"); } //判断密码是否输入,如果输入了,再判断格式是否正确 if( password == null —— "".equals(password.trim())){ this.addFieldError("password", "密码必须输入"); }else if( ! Pattern.matches("\\w{6,20}", password.trim())) { this.addFieldError("password", "密码必须是字母和数字,长度为6到20之间"); } //判断确认密码是否输入,如果输入了,再判断格式是否正确 if(repassword == null —— "".equals(repassword.trim())){ this.addFieldError("repassword", "确认密码必须输入"); }else if( ! Pattern.matches("\\w{6,20}", repassword.trim())) { this.addFieldError("repassword", "确认密码必须是字母和数字,长度为6到20之间"); } //判断确认密码和密码是否相同 if(password ! = null && repassword ! = null && ! repassword.equals(password)){ this.addFieldError("repassword", "确认密码和密码必须相同"); } //判断年龄是否合法 if(age < 0 —— age >130) { this.addFieldError("age", "请输入有效的年龄"); } //判断出生日期是否合法 Calendar start = Calendar.getInstance(); Calendar end = Calendar.getInstance(); start.set(1900, 1,1); end.set(2010, 1,1); if(birth ! = null && ( birth.after(end.getTime()) —— birth.before(start.getTime()))) { this.addFieldError("birth", "请输入有效的出生日期"); } //判断邮箱地址是否合法 if(email ! = null && ! "".equals(email) && email ! = "" && ! Pattern.matches ("[a-zA-Z][a-zA-Z0-9._-]*@([a-zA-Z0-9-_]+\\.)+(com—gov—net—com\\.cn—edu\\.cn)", email)){ this.addFieldError("email", "请输入正确的邮箱地址"); } }
同时要修改用户注册页,将<s:actionerror>表单删除。再次运行用户注册页,输入一些非法的信息。单击“注册”按钮,页面将会提示错误信息,同时会在相应的表单元素上显示错误信息,如图4.15所示。
这样的错误提示信息就比较友好了,用户能清楚地知道是哪个信息输入错误。
4.3.6 输入校验与类型转换关系
下面来看输入校验和类型转换之间的关系。首先打开上面的注册页面,在年龄文本框中输入一个非法的信息,如“aa”。这时页面将显示错误信息,如图4.16所示。
图4.15 提示错误信息
图4.16 提示错误信息
在年龄文本框上显示的错误信息是类型转换出错的错误信息。因为age属性的类型为int,输入一个aa字符串将导致类型转换失败,所以将提示类型转换错误信息。那么类型转换和输入校验哪个先执行呢?它们之间有什么关系呢?
用户提交参数后,首先将会调用类型转换器来执行类型转换。如果类型转换失败,则会将错误信息保存在fieldError中。然后不管类型转换失败还是成功都会进行输入校验,并将校验错误信息保存到fieldError或者actionError中。最后系统判断fieldError或者actionError中是否存在错误信息,如果存在错误信息,页面将跳转到input逻辑视图指定的视图资源,并在视图资源中将错误信息显示出来。
类型转换和输入校验的执行顺序是非常重要的。为什么要先进行类型转换呢。因为不能保证用户输入的信息符合要求,比如年龄属性肯定会要求接受一个能够转换为int类型的字符串。用户的输入是自由的,很可能输入一些无法转换的字符串,所以必须先对用户输入的信息进行类型转换,然后再将类型转后的数据进行输入校验。
那如果在年龄文本框中输入一个“aa”字符串,类型转换肯定会失败,在fieldError中将保存该类型转换错误信息。age属性的初始值为0,那么在输入校验中也将提示该错误信息。下面来看如下代码段。
if(age < 0 —— age >130) { this.addFieldError("age", "请输入有效的年龄"); }
在这里判断只有当age小于0或者大于130,才会提示输入校验错误信息。age为0肯定是不行的!那将代码修改成age<=0。同样在“年龄”文本框中输入一个非法的信息,如“aa”。这时页面将显示错误信息,如图4.17所示。
图4.17 提示错误信息
从页面中可以看出,页面提示了两条错误信息。那为什么会出现两条错误信息呢?
首先类型转换失败,所以会添加类型转换错误信息。然后又进行了输入校验,因为age赋值为0,所以也会添加输入校验错误信息。这样就会显示两条错误信息。
虽然提示两条错误信息,但是能有效地防止用户在年龄参数中提交0值。如果在输入校验中放宽一下,对age属性为0不提示错误信息,这样就能防止出现两条错误信息。
那如何处理好类型转换错误信息和输入校验错误信息呢?
通常在转换的时候做格式的校验,在Action中的校验方法中校验取值。假如输入错误格式,可以通过类型转换错误信息(当然可以在资源文件配置invalid.fieldvalue.xxx来提示自定义的错误信息)来提示用户正确的格式。如果是取值问题(比如年龄取值),假如用户输入180,这样就可以通过输入校验方法添加错误信息来提示用户输入正确的信息。