1.表单校验的必要性

    基于Web的应用,必须对用户提交的表单进行前台和后台的校验。
前台验证主要看需求和用户体验。用户输出错误的表单参数,可以马上得到提醒,同时减轻服务器压力。
后台验证注重的是安全性。防止恶意用户向后台提交非法数据。
 
    后台验证是必要的,前台验证是充分的。
    前台的校验通常是通过html,javascript等前台的技术来限制用户的输入,并对用户做出友好的提示和建议,它的最大好处是快。在表单数据尚未提交到后台应用程序即能做出校验并提示给用户,用户可以更改或者重新输入数据。这种方式的用户体验好。但是,仅仅前台的校验是不能保证提交数据的合法性的。尽管现在有一些前台脚本的压缩和加密技术可以初步模糊前台的脚本,不过通过一定的手段是可以恢复本来面目的。这样我们的前台就完全暴露在用户面前了。用户就可以通过firebug之类的工具篡改前台页面和数据,甚至可以向后台直接提交非法数据。如果后台不再对数据进行校验,轻则程序会崩掉,重则被不良企图者利用,这会是致命的。
后台校验通常是对客户端提交的表单进行逻辑校验。校验失败则向前台返回错误信息,校验通过之后才允许进一步执行程序。后台校验需要用户提交表单之后,才进行校验,能有效地保证提交数据的合法性。后台校验的用户体验较差,同时在后台频繁地校验表单参数,加重了服务端的负担。
    所以,对于一个Web项目,前台和后台的表单校验,都是必不可少的。
 
2. 前后台校验的统一

2.1. 增删改查

2.1.1. 查询

Web项目最常用的操作是增删改查。

查询操作应使用Http GET提交表单,增加、删除、修改操作应该使用Http POST提交表单。

查询操作的主要漏洞是SQL注入。为了防止SQL注入,Web应用程序访问数据库时,不应该使用字符串拼接的形式。

参数化查询可以防止SQL注入。

参数化查询(Parameterized Query Parameterized Statement)是访问数据库时,在需要填入数值或数据的地方,使用参数 (Parameter) 来给值。

即使用户传入非法数据,查询结果会为空,并不会更改数据库。

所以,查询操作只需进行前台校验,表单提交方式为GET

2.1.2. 增删改

增删改操作会更改数据库。为了防止恶意用户插入非法数据,必须在后台进行表单校验。同时,为了加强用户体验和减轻服务器负担,需要结合前台校验。

所以,增删改操作应进行前后台校验,表单提交方式为POST

2.2 字符集编码

使用不同的字符集编码,汉字的长度进行不同处理。

如果前台使用GBK编码,表单提交到后台服务端时,在进行编码转换之前,汉字会存储为两个字符。

然而,在进行前台校验时,浏览器(IE9, Firefox, Google Chrome)把汉字处理成一个字符。

数据库的字符串字段类型,建议选用nvarcharnchar。因为varcharchar会把汉字存储为两个字符。My SQL, SQL serverOracle均支持nvarcharnchar

前台、后台和数据库对汉字长度的处理不一致,是一件很让开发人员头疼的事情。

为了解决这个问题,客户端、服务端和数据库,应该统一使用utf-8编码,所有的字符(包括中文)都应该处理为1

3. 前后台检验的具体实现样例

前台使用jquery.validatehtml进行校验。后台校验整合了Spring 3 ValidatorHibernate Validator

Tips: JSR-303是一个接口标准,并不是Spring框架的一部分。Spring 3支持了JSR-303标准。Hibernate ValidatorJSR-303的一个实现。

一个表单对应一个后台FormBean, 即使表单只有一个参数,我们也应该创建一个FormBean。前台页面通过HTTP POST提交的数据,都应该直接导入FormBean中。尽可能地避免req.getParameter(...)的写法。

创建部门的ftl页面代码:


   
  1. <#-- 本页的标题区 --> 
  2. <h1>新建部门账户</h1> 
  3. <#-- tips --> 
  4. <#if tips??> 
  5.   <#if tips == "操作成功"> 
  6.     <div class="alert alert-success"> 
  7.       <a class="close" data-dismiss="alert">×</a> 
  8.       ${tips}  <#-- 提示信息  --> 
  9.     </div> 
  10.   <#else> 
  11.     <div class="alert alert-block alert-error"> 
  12.       <a class="close" data-dismiss="alert">×</a> 
  13.       <h4 class="alert-heading">错误</h4>  <#--  提示信息标题  --> 
  14.       ${tips}  <#-- 提示信息  --> 
  15.     </div> 
  16.   </#if> 
  17. </#if> 
  18. <#-- 本页内容区 --> 
  19. <form class="form-horizontal" id="objBean" name="objBean" action="market/org/createorg" method="POST"> 
  20.   <fieldset> 
  21.     <div class="control-group"> 
  22.       <label class="control-label" for="customerId">客户名称</label> 
  23.       <div class="controls"> 
  24.         <input class="span5" type="hidden" id="customerId" name="customerId" value="${custBean.customerId}"/> 
  25.         <input class="span5" id="customerId_name" name="customerId_name" value="${custBean.customerName?default('')}(${custBean.customerId?default('')})" disabled/> 
  26.       </div> 
  27.     </div> 
  28.     <div class="control-group"> 
  29.       <label class="control-label" for="departmentId"><em class="required">*</em>部门账户编号</label> 
  30.       <div class="controls"> 
  31.         <input class="span5" type="text" id="departmentId" name="departmentId" maxlength="16"/> 
  32.         <p class="help-block">部门账户 的编号,最长不超过16位字符。</p> 
  33.       </div> 
  34.     </div> 
  35.     <div class="control-group"> 
  36.       <label class="control-label" for="departmentId"><em class="required">*</em>部门账户名称</label> 
  37.       <div class="controls"> 
  38.         <input class="span5" type="text" id="departmentName" name="departmentName" maxlength="32"/> 
  39.         <p class="help-block">部门账户的名称,最长不超过32位字符。</p> 
  40.       </div> 
  41.     </div>     
  42.     <#-- 操作按钮区 --> 
  43.     <div class="control-group"> 
  44.       <div class="controls"> 
  45.         <input class="btn btn-large" id="submit_button" type="submit" value="确认"/> 
  46.         <a class="btn btn-large" href="market/org/listorg/query">取消</a> 
  47.       </div> 
  48.     </div> 
  49.   </fieldset> 
  50. </form> 
  51. <#-- 本页JS代码区 --> 
  52. <script type="text/javascript"> 
  53. function initialize(){ 
  54.   $('#objBean').validate({ 
  55.     rules : { 
  56.       departmentId: { required : true, maxlength:16 }, 
  57.       departmentName: { required : true, maxlength:32 }                  
  58.     } 
  59.   }); 
  60. </script> 

 

部门Controller里面的创建部门POST方法:

 


   
  1. /* 
  2.      * 新增部门账户,post方法。 
  3.      */ 
  4.     @SuppressWarnings("unchecked"
  5.     @RequestMapping(value = "/market/org/createorg", method = RequestMethod.POST) 
  6.     public ModelAndView createOrg(@ModelAttribute("map") HashMap<String, Object> map, 
  7.             @Valid CreateOrgForm orgForm, BindingResult result,  
  8.             HttpServletRequest req, HttpServletResponse res) { 
  9.         //表单参数校验 
  10.         if(result.hasErrors()){ 
  11.             req.setAttribute("tips""表单参数有误,请检查后重新提交!"); 
  12.             return this.listAllOrganization(req, res, "query"); 
  13.         } 
  14.         ModelAndView mav = new ModelAndView(); 
  15.         OrganizationBean bean = new OrganizationBean(); 
  16.         bean.setCustomerId(orgForm.getCustomerId()); 
  17.         bean.setCustomerName(orgForm.getCustomerId_name()); 
  18.         bean.setDepartmentId(orgForm.getDepartmentId()); 
  19.         bean.setDepartmentName(orgForm.getDepartmentName()); 
  20.          
  21.         ResponseBean responseBean = orgMgrService.createOrg(bean); 
  22.         //记录日志 
  23.         map.put("resBean", responseBean); 
  24.         if (!responseBean.getResultCode().equals("0")) { 
  25.  
  26.             mav.addObject("tips", responseBean.getResultDec()); 
  27.             // 查询客户 
  28.             responseBean = orgMgrService.listAllCustomer(); 
  29.  
  30.             List<CustomerBean> custList = null
  31.             if (CommonConst.OPER_SUCCESS_CODE.equals(responseBean 
  32.                     .getResultCode())) { 
  33.                 custList = (List<CustomerBean>) responseBean.getResultObj(); 
  34.                 mav.addObject("custList", custList); 
  35.             } else { 
  36.                 ; 
  37.             } 
  38.  
  39.             mav.addObject("ftlName""organization/OrgCreateMgr.ftl"); 
  40.             mav.setViewName("/mainfrm.ftl"); 
  41.             return mav; 
  42.         } else { 
  43.             req.setAttribute("departmentName"null); 
  44.  
  45.             return listAllOrganization(req, res, "query"); 
  46.         } 
  47.     } 

 

方法里的参数CreateOrgForm orgForm是表单参数,它是一个FormBean。前台提交的表单参数会自动匹配CreateOrgForm 中的属性名,并把相应的数据传入CreateOrgForm 的属性值中。CreateOrgForm的属性名必须和前台表单的标签id相同。

在参数CreateOrgForm orgForm前添加注解@Valid。通过@Valid注解,Spring MVC会根据FormBean的限制条件,进行数据校验。校验结果设置进紧跟其后的BindingResult result参数中。

Controller方法体的最前面,插入以下代码:

 


   
  1. //表单参数校验 
  2.         if(result.hasErrors()){ 
  3.             req.setAttribute("tips""表单参数有误,请检查后重新提交!"); 
  4.             return this.listAllOrganization(req, res, "query"); 
  5.         } 

    后台校验出错时,我们并不需要返回具体错误。因为后台校验主要是基于安全考虑的,不应该给恶意用户提供太多的信息。表单参数已经在前台进行校验,并且会在前台进行相应的提示。在用户正常操作的前提下,经过了前台校验的表单,一定能通过后台验证的。

    创建部门的FormBean代码如下:

    


   
  1. package com.sunguard.mvc.storage.market.formbean; 
  2.  
  3. import org.hibernate.validator.constraints.Length; 
  4. import org.hibernate.validator.constraints.NotBlank; 
  5.  
  6. public class CreateOrgForm { 
  7.     @NotBlank 
  8.     private String customerId; 
  9.     private String customerId_name; 
  10.     @NotBlank 
  11.     private String departmentId; 
  12.     @NotBlank 
  13.     @Length(max=32
  14.     private String departmentName; 
  15.      
  16.     public String getCustomerId() { 
  17.         return customerId; 
  18.     } 
  19.     public void setCustomerId(String customerId) { 
  20.         this.customerId = customerId; 
  21.     } 
  22.     public String getCustomerId_name() { 
  23.         return customerId_name; 
  24.     } 
  25.     public void setCustomerId_name(String customerId_name) { 
  26.         this.customerId_name = customerId_name; 
  27.     } 
  28.     public String getDepartmentId() { 
  29.         return departmentId; 
  30.     } 
  31.     public void setDepartmentId(String departmentId) { 
  32.         this.departmentId = departmentId; 
  33.     } 
  34.     public String getDepartmentName() { 
  35.         return departmentName; 
  36.     } 
  37.     public void setDepartmentName(String departmentName) { 
  38.         this.departmentName = departmentName; 
  39.     } 
  40.      

 

FormBean的属性前使用的限制注解,如@NotBlank@Length(max=32)等,是Hibernate Validator的注解。

 

总结:

1.  前台和后台验证都必不可少。前台校验侧重于用户体验和减轻服务器负担,后台校验更注重安全性。

2.  查询操作使用HTTP GET提交表单,Web程序的查询应该为参数化查询。

3.  增删改操作使用HTTP POST提交表单,每个表单对应一个FormBean,在FormBean中添加Hibernate Validator的限制注解。

4.  Controller中的POST方法参数中,表单参数前面必须添加@Valid注解

5.  BindResult result参数必须紧跟表单参数之后。

6.  FormBean的属性名必须与前台表单里面的标签id相同。

7.  表单校验出错,只需把错误信息返回到当前模块的查询页面。Tips通过req.setAttribute("tips""表单参数有误,请检查后重新提交!");.语句设置 

进一步建议:

与记录日志的整合

每个表单都对应一个FormBean,任何增删改操作都应该在日志中留下记录,即每次进入POST方法都必须记录日志。

我们可以定义一个抽象父类LogInfo 

 

  1. public abstract LogInfo{  
  2.     private String operType;  
  3.     private String operName;  
  4.     private String operDescripton;  
  5.     private int operResultCode;  
  6.  
  7.     //getter and setters... 
  8. } 

 

 

然后,每个FormBean都去继承LogInfo.

在每个ControllerPOST方法里面,通过set方法为这四个父类属性设值。记录日志所需要的值可以从这四个属性中获取。

 

 

Spring ValidatorHibernate Validator的比较:

Spring Framework自带的validation的做法是,继承父类Validator,为每个FormBean绑定一个校验类。

具体做法如下:

 


   
  1. public class Person {  
  2.     private String name;  
  3.     private int age;  
  4.     // the usual getters and setters...  
  5. }  

 

对应的校验类如下:


   
  1. public class PersonValidator implements Validator {  
  2.     public boolean supports(Class clazz) {  
  3.         return Person.class.equals(clazz);  
  4.     }  
  5.     public void validate(Object obj, Errors e) {  
  6.         ValidationUtils.rejectIfEmpty(e, "name""name.empty");  
  7.         Person p = (Person) obj;  
  8.         if (p.getAge() < 0) {  
  9.             e.rejectValue("age""negativevalue");  
  10.         } else if (p.getAge() > 110) {  
  11.             e.rejectValue("age""too.darn.old");  
  12.         }  
  13.     }  
  14. }  

引入校验类,在包结构上,我们必须添加Validator这一层。

 

相比之下,我更倾向于SpringHibernate Validator整合的做法。

Spring 3 支持JSR-303 Bean Validation API

JSR-303是一个接口标准,它并不是Spring Framework 的一部分。

Hibernate ValidatorJSR-303的一个实现。在FormBean里添加Hibernate Validator的注解,与定义一个校验类的做法相比。注解更加简洁、灵活。

Hibernate Validator 4.3.0依赖的Jar包如下:

hibernate-validator-4.3.0.Final.jar

validation-api-1.0.0.GA.jar

jboss-logging-3.1.0.CR2.jar