1.表单校验的必要性
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)把汉字处理成一个字符。
数据库的字符串字段类型,建议选用nvarchar和nchar。因为varchar和char会把汉字存储为两个字符。My SQL, SQL server和Oracle均支持nvarchar和nchar。
前台、后台和数据库对汉字长度的处理不一致,是一件很让开发人员头疼的事情。
为了解决这个问题,客户端、服务端和数据库,应该统一使用utf-8编码,所有的字符(包括中文)都应该处理为1。
3. 前后台检验的具体实现样例
前台使用jquery.validate和html进行校验。后台校验整合了Spring 3 Validator和Hibernate Validator。
Tips: JSR-303是一个接口标准,并不是Spring框架的一部分。Spring 3支持了JSR-303标准。Hibernate Validator是JSR-303的一个实现。
一个表单对应一个后台FormBean, 即使表单只有一个参数,我们也应该创建一个FormBean。前台页面通过HTTP POST提交的数据,都应该直接导入FormBean中。尽可能地避免req.getParameter(...)的写法。
创建部门的ftl页面代码:
- <#-- 本页的标题区 -->
- <h1>新建部门账户</h1>
- <#-- tips -->
- <#if tips??>
- <#if tips == "操作成功">
- <div class="alert alert-success">
- <a class="close" data-dismiss="alert">×</a>
- ${tips} <#-- 提示信息 -->
- </div>
- <#else>
- <div class="alert alert-block alert-error">
- <a class="close" data-dismiss="alert">×</a>
- <h4 class="alert-heading">错误</h4> <#-- 提示信息标题 -->
- ${tips} <#-- 提示信息 -->
- </div>
- </#if>
- </#if>
- <#-- 本页内容区 -->
- <form class="form-horizontal" id="objBean" name="objBean" action="market/org/createorg" method="POST">
- <fieldset>
- <div class="control-group">
- <label class="control-label" for="customerId">客户名称</label>
- <div class="controls">
- <input class="span5" type="hidden" id="customerId" name="customerId" value="${custBean.customerId}"/>
- <input class="span5" id="customerId_name" name="customerId_name" value="${custBean.customerName?default('')}(${custBean.customerId?default('')})" disabled/>
- </div>
- </div>
- <div class="control-group">
- <label class="control-label" for="departmentId"><em class="required">*</em>部门账户编号</label>
- <div class="controls">
- <input class="span5" type="text" id="departmentId" name="departmentId" maxlength="16"/>
- <p class="help-block">部门账户 的编号,最长不超过16位字符。</p>
- </div>
- </div>
- <div class="control-group">
- <label class="control-label" for="departmentId"><em class="required">*</em>部门账户名称</label>
- <div class="controls">
- <input class="span5" type="text" id="departmentName" name="departmentName" maxlength="32"/>
- <p class="help-block">部门账户的名称,最长不超过32位字符。</p>
- </div>
- </div>
- <#-- 操作按钮区 -->
- <div class="control-group">
- <div class="controls">
- <input class="btn btn-large" id="submit_button" type="submit" value="确认"/>
- <a class="btn btn-large" href="market/org/listorg/query">取消</a>
- </div>
- </div>
- </fieldset>
- </form>
- <#-- 本页JS代码区 -->
- <script type="text/javascript">
- function initialize(){
- $('#objBean').validate({
- rules : {
- departmentId: { required : true, maxlength:16 },
- departmentName: { required : true, maxlength:32 }
- }
- });
- }
- </script>
部门Controller里面的创建部门POST方法:
- /*
- * 新增部门账户,post方法。
- */
- @SuppressWarnings("unchecked")
- @RequestMapping(value = "/market/org/createorg", method = RequestMethod.POST)
- public ModelAndView createOrg(@ModelAttribute("map") HashMap<String, Object> map,
- @Valid CreateOrgForm orgForm, BindingResult result,
- HttpServletRequest req, HttpServletResponse res) {
- //表单参数校验
- if(result.hasErrors()){
- req.setAttribute("tips", "表单参数有误,请检查后重新提交!");
- return this.listAllOrganization(req, res, "query");
- }
- ModelAndView mav = new ModelAndView();
- OrganizationBean bean = new OrganizationBean();
- bean.setCustomerId(orgForm.getCustomerId());
- bean.setCustomerName(orgForm.getCustomerId_name());
- bean.setDepartmentId(orgForm.getDepartmentId());
- bean.setDepartmentName(orgForm.getDepartmentName());
- ResponseBean responseBean = orgMgrService.createOrg(bean);
- //记录日志
- map.put("resBean", responseBean);
- if (!responseBean.getResultCode().equals("0")) {
- mav.addObject("tips", responseBean.getResultDec());
- // 查询客户
- responseBean = orgMgrService.listAllCustomer();
- List<CustomerBean> custList = null;
- if (CommonConst.OPER_SUCCESS_CODE.equals(responseBean
- .getResultCode())) {
- custList = (List<CustomerBean>) responseBean.getResultObj();
- mav.addObject("custList", custList);
- } else {
- ;
- }
- mav.addObject("ftlName", "organization/OrgCreateMgr.ftl");
- mav.setViewName("/mainfrm.ftl");
- return mav;
- } else {
- req.setAttribute("departmentName", null);
- return listAllOrganization(req, res, "query");
- }
- }
方法里的参数CreateOrgForm orgForm是表单参数,它是一个FormBean。前台提交的表单参数会自动匹配CreateOrgForm 中的属性名,并把相应的数据传入CreateOrgForm 的属性值中。CreateOrgForm的属性名必须和前台表单的标签id相同。
在参数CreateOrgForm orgForm前添加注解@Valid。通过@Valid注解,Spring MVC会根据FormBean的限制条件,进行数据校验。校验结果设置进紧跟其后的BindingResult result参数中。
在Controller方法体的最前面,插入以下代码:
- //表单参数校验
- if(result.hasErrors()){
- req.setAttribute("tips", "表单参数有误,请检查后重新提交!");
- return this.listAllOrganization(req, res, "query");
- }
后台校验出错时,我们并不需要返回具体错误。因为后台校验主要是基于安全考虑的,不应该给恶意用户提供太多的信息。表单参数已经在前台进行校验,并且会在前台进行相应的提示。在用户正常操作的前提下,经过了前台校验的表单,一定能通过后台验证的。
创建部门的FormBean代码如下:
- package com.sunguard.mvc.storage.market.formbean;
- import org.hibernate.validator.constraints.Length;
- import org.hibernate.validator.constraints.NotBlank;
- public class CreateOrgForm {
- @NotBlank
- private String customerId;
- private String customerId_name;
- @NotBlank
- private String departmentId;
- @NotBlank
- @Length(max=32)
- private String departmentName;
- public String getCustomerId() {
- return customerId;
- }
- public void setCustomerId(String customerId) {
- this.customerId = customerId;
- }
- public String getCustomerId_name() {
- return customerId_name;
- }
- public void setCustomerId_name(String customerId_name) {
- this.customerId_name = customerId_name;
- }
- public String getDepartmentId() {
- return departmentId;
- }
- public void setDepartmentId(String departmentId) {
- this.departmentId = departmentId;
- }
- public String getDepartmentName() {
- return departmentName;
- }
- public void setDepartmentName(String departmentName) {
- this.departmentName = departmentName;
- }
- }
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。
然后,每个FormBean都去继承LogInfo.
在每个Controller的POST方法里面,通过set方法为这四个父类属性设值。记录日志所需要的值可以从这四个属性中获取。
Spring Validator和Hibernate Validator的比较:
Spring Framework自带的validation的做法是,继承父类Validator,为每个FormBean绑定一个校验类。
具体做法如下:
- public class Person {
- private String name;
- private int age;
- // the usual getters and setters...
- }
对应的校验类如下:
- public class PersonValidator implements Validator {
- public boolean supports(Class clazz) {
- return Person.class.equals(clazz);
- }
- public void validate(Object obj, Errors e) {
- ValidationUtils.rejectIfEmpty(e, "name", "name.empty");
- Person p = (Person) obj;
- if (p.getAge() < 0) {
- e.rejectValue("age", "negativevalue");
- } else if (p.getAge() > 110) {
- e.rejectValue("age", "too.darn.old");
- }
- }
- }
引入校验类,在包结构上,我们必须添加Validator这一层。
相比之下,我更倾向于Spring和Hibernate Validator整合的做法。
Spring 3 支持JSR-303 Bean Validation API。
JSR-303是一个接口标准,它并不是Spring Framework 的一部分。
Hibernate Validator是JSR-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