使用SSM重构Bookstore——表单验证

一、前端验证

重构前是用JQuery的validation插件在前端验证,但这种方式不安全可通过一定手段跳过检查

//官方网站有点乱,菜鸟的教程还可以 https://www.runoob.com/jquery/jquery-plugin-validate.html
//  javascript哪儿写得不对执行就会走形很让人抓狂,是不是太过依赖IDE语法检查了?
<script type="text/javascript">
//添加验证方法
jQuery.validator.addMethod("isPhoneNumber", function(value,element) {   
	var length = value.length;   
	var mobile = /^(((13[0-9])|(15[0-9])|(18[0-9]))+\d{8})$/;   
	var tel = /^0\d{2,3}-?\d{7,8}$/g;       
	return this.optional(element) || tel.test(value) || (length==11 && mobile.test(value));   
}, "请正确填写您的联系方式"); 

//表单验证
$("#registerUserForm").validate({
	//debug:true,
	onkeyup: false,
	focusCleanup:true,
	rules:{  //规则
		username:{
			required: true,
			minlength: 6,
			remote: {
				type: "POST",
				url: "${pageContext.request.contextPath}/checkUsername",
				data: {"username": function(){return $("#username").val();}},
				dataType:"json"
			}
		},
		email:{
			required: true,
			email: true
		},
		password:{
			required: true,
			minlength: 6
		},
		repassword:{
			required: true,
			minlength: 6,
			equalTo: "#password"
		},
        telephone:{
			required: true,
			isPhoneNumber:true
		}
	},
	messages:{ //错误提示信息
		username:{
			required: "用户名不能为空",
			minlength: "长度至少6位",
			remote: "此用户名已被占用"
		},
		email:{
			required: "邮箱不能为空",
			email: "请填写正确格式的邮箱",
		},
		password:{
			required: "密码不能为空",
			minlength: "密码长度至少6位"
		},
		repassword:{
			required: "密码不能为空",
			minlength: "密码长度至少6位",
			equalTo: "两次密码输入不一致"
		},
        telephone:{
			required: "电话不能为空",
			isPhoneNumber:"请填写正确的电话号码或手机号"
		}
	},
    errorPlacement: function(error, element) {  
			error.appendTo(element.parent());     //信息显示位置
	}
});

二、后端验证

1.字符集

从浏览器发过来的数据解析后都是字符串文本流,故而参数解析首先需要注意字符集问题,这又与Web服务器有关,以前我们得自己配置(tomcat官方FAQ中关于使用UTF-8)

Using UTF-8 as your character encoding for everything is a safe bet. This should work for pretty much every situation.

In order to completely switch to using UTF-8, you need to make the following changes:

  1. Set URIEncoding="UTF-8" on your in server.xml. References: HTTP Connector, AJP Connector. Tomcat8后如果strict servlet compliance 没有启用,会自动设为UTF-8

  2. Set the default request character encoding either in the Tomcat conf/web.xml file or in the web app web.xml file; either by setting or by using a character encoding filter.

  3. Change all your JSPs to include charset name in their contentType.
    For example, use <%@page contentType="text/html; charset=UTF-8" %> for the usual JSP pages and for the pages in XML syntax (aka JSP Documents).

  4. Change all your servlets to set the content type for responses and to include charset name in the content type to be UTF-8.
    Use response.setContentType("text/html; charset=UTF-8")orresponse.setCharacterEncoding("UTF-8").

  5. Change any content-generation libraries you use (Velocity, Freemarker, etc.) to use UTF-8 and to specify UTF-8 in the content type of the responses that they generate.

  6. Disable any valves or filters that may read request parameters before your character encoding filter or jsp page has a chance to set the encoding to UTF-8. For more information see http://www.mail-archive.com/[email protected]/msg21117.html.

使用Spring后直接用就好了


<filter>
    <filter-name>encodingUTF8filter-name>
    <filter-class>org.springframework.web.filter.CharacterEncodingFilterfilter-class>
    <init-param>
        <param-name>encodingparam-name>
        <param-value>UTF-8param-value>
    init-param>
filter>
<filter-mapping>
    <filter-name>encodingUTF8filter-name>
    <url-pattern>/*url-pattern>   
filter-mapping>

2.数据绑定

使用SSM重构Bookstore——表单验证_第1张图片
使用SSM重构Bookstore——表单验证_第2张图片
参数正确解析后,需要将代表参数值的字符串按需转换类型,这就牵扯到数据格式化和数据校验

如果需要自定义处理可以参看下面链接

  • SpringMVC中的参数绑定总结

  • spring MVC处理请求过程

  • SpringMVC Databinding(数据绑定)

  • Spring MVC的数据转换及数据格式化

  • SpringMVC(八)数据转换 & 数据格式化 & 数据校验

  • SpringMVC数据格式化——第七章 注解式控制器的数据验证、类型转换及格式化——跟着开涛学SpringMVC

2.1 数据格式化

By default formatters, for Number and Date types are installed, including support for
the @NumberFormat and @DateTimeFormat annotations. Full support for the Joda-Time
formatting library is also installed if Joda-Time is present on the classpath.

  • DateTimeFormat:

因为其用法比较单一,只用于将字符串格式化成日期,在加入spring以后,直接使用注解@DateTimeFormat(pattern=”yyyy-MM-dd”)即可。@DateTimeFormat 注解有3个可选的属性:style,pattern和iso

属性style: 允许我们使用两个字符的字符串来表明怎样格式化日期和时间。第一个字符表明了 日期的格式,第二个字符表明了时间的格式。

描述 字符串值 示例输出
短格式(这是缺省值) SS 8/30/64 11:24 AM
中等格式 MM Aug 30, 1964 11:24:41 AM
长格式 LL August 30, 1964 11:24:41 AM CDT
完整格式 FF Sunday, August 30,1964 11:24:41 AM CDT
使用短横线省略日期或时间 M- Aug 30, 1964

Pattern: 属性允许我们使用自定义的日期/时间格式。该属性的值遵循java标准的date/time格式规范。缺省的该属性的值为空,也就是不进行特殊的格式化。通常情况下我们都是使用这个 注解做自定义格式化的。
iso: 基本上用不上,这里不做讲解

  • NumberFormat

@NumberFormat(pattern="#,###") 用来格式化货币(这样前端得传形如1,000而不能是1000了)

顺便再提下输出格式化

@JsonFormat(pattern=“yyyy-MM-dd”) 将Date转换成String 一般后台传值给前台时
此处注意:@JsonFormat会让时间以0区时间显示。如果直接使用会少了8小时(北京时区)修改为@JsonFormat(pattern=“yyyy-MM-dd”,timezone=“GMT+8”)

2.2 数据校验

官方文档Spring validation部分感觉有点简略,通过Goolge发现个不错的网站HowToDoInJava,资料可能有点旧了但例子很好,当然网上还有很多培训教程——我感觉这个讲得还不错可以看看(上面图片就来自于此)

Spring Validation

Spring 3 introduced several enhancements to its validation support. First, the JSR-303 Bean Validation API is fully supported. Second, when used programmatically, Spring’s DataBinder can validate objects as well as bind to them. Third, Spring MVC has support for declaratively validating @Controller inputs.

A JSR-303 or JSR-349 provider, such as the Hibernate Validator, is expected to be present in the classpath and is automatically detected.

For general information on JSR-303 and JSR-349, see the Bean Validation website. For information on the specific capabilities of the default reference implementation, see the Hibernate Validator documentation.

Spring Framework 5中文文档 不全

要完成后端验证首先需要添加相关依赖


<dependency>
    <groupId>javax.validationgroupId>
    <artifactId>validation-apiartifactId>
    <version>2.0.1.Finalversion>
dependency>


<dependency>
    <groupId>org.hibernategroupId>
    <artifactId>hibernate-validatorartifactId>
    <version>6.0.16.Finalversion>
dependency>


各种校验例子 https://blog.csdn.net/u013815546/article/details/77248003

@Validated和@Valid区别
https://blog.csdn.net/qq_27680317/article/details/79970590
在检验Controller的入参是否符合规范时,使用@Validated或者@Valid在基本验证功能上没有太多区别。
@Validated:提供了一个分组功能,可以在入参验证时,根据不同的分组采用不同的验证机制
@Validated和@Valid相比不能用在成员属性(字段)上
在需要嵌套验证的属性上添加@Valid注解,入参时验证时两者均可

JSR提供的校验注解:         
@Null   被注释的元素必须为 null    
@NotNull    被注释的元素必须不为 null    
@AssertTrue     被注释的元素必须为 true    
@AssertFalse    被注释的元素必须为 false    
@Min(value)     被注释的元素必须是一个数字,其值必须大于等于指定的最小值    
@Max(value)     被注释的元素必须是一个数字,其值必须小于等于指定的最大值    
@DecimalMin(value)  被注释的元素必须是一个数字,其值必须大于等于指定的最小值    
@DecimalMax(value)  被注释的元素必须是一个数字,其值必须小于等于指定的最大值    
@Size(max=, min=)   被注释的元素的大小必须在指定的范围内    
@Digits (integer, fraction)     被注释的元素必须是一个数字,其值必须在可接受的范围内    
@Past   被注释的元素必须是一个过去的日期    
@Future     被注释的元素必须是一个将来的日期    
@Pattern(regex=,flag=)  被注释的元素必须符合指定的正则表达式    

Hibernate Validator提供的校验注解:  
@NotBlank(message =)   验证字符串非null,且长度必须大于0 
@Email  被注释的元素必须是电子邮箱地址   
@Length(min=,max=)  被注释的字符串的大小必须在指定的范围内    
@NotEmpty   被注释的字符串的必须非空     
@Range(min=,max=,message=)  被注释的元素必须在合适的范围内

三、实例

用户注册部分 —— 在需要校验的属性上添加注解,都是通过表单提交过来的

package com.bookstore.Domain;

import org.hibernate.validator.constraints.Email;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.Past;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
import org.springframework.format.annotation.DateTimeFormat;
import java.util.Date;

public class User {
    private String id;   //在controller中设置

    @NotNull
    @Length(min=6)
    private String username;

    @NotNull   //还可以加(message="...")
    @Length(min=6)
    private String password;

    @NotNull
    @Email    //这个注解似乎要被废弃了
    private String email;

    @NotNull
    @Pattern(regexp = "^((13[0-9]|15[012356789]|18[0-9])[0-9]{8})$|^(0\\d{2,3}-?\\d{7,8})$")     //校验手机号和固话
    private String telephone;
    
    private Date registtime;   //在controller中设置
 
    private String realname;   //可以为空

    private String gender;   //数据库设置了默认值

	@Past
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date birthday;

    private String activecode;   //在controller中设置

    private Byte state;     //在controller中设置

    private String role;    //数据库设置了默认值
    
    // setter和getter部分省略......

要将后端的校验错误信息显示到前端可以参考官方在github的示例spring-mvc showcase,我偷下懒不想改表单调样式前端的错误提示也可以用嘛,感觉使用Spring后代码精简很多也更方便集成

UserController.java

package com.bookstore.Controller;

import com.bookstore.Domain.User;
import com.bookstore.Exception.UserException;     //自定义Exception
import com.bookstore.Service.UserService;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpSession;
import javax.validation.Valid;
import java.util.Date;
import java.util.List;
import java.util.UUID;

@Controller
public class UserController {

    @Autowired
    private UserService userService;

    @GetMapping("/login")
    public String login() {
        return "login";
    }

    @PostMapping("/login")
    public String login(@RequestParam String username, @RequestParam String password, @RequestParam(required = false) String returnURL, HttpSession session) throws UserException {
        User user = userService.login(username, password);
        session.setAttribute("user", user);
        if(returnURL != null) return "redirect:" + returnURL;
        return "redirect:index";
    }

    @GetMapping("/logout")
    public String logout(HttpSession session) {
        session.invalidate();
        return "redirect:index";
    }

    @GetMapping("/register")
    public String register() {
        return "register";
    }
    
    @PostMapping("/register")
    public String register(@Valid User user, Model model, BindingResult result) throws UserException {
        //注意方法参数里的@Valid要求数据校验,可以循环嵌套验证——类比级联
        //显示数据绑定错误
        if (result.hasErrors()) {
            List<ObjectError> errors = result.getAllErrors();
            for (ObjectError error : errors) {
                System.out.println(error.getDefaultMessage());
            }
        }
        
        //设置用户信息,用户密码最好再处理下不要直接保存,这里简化了
        user.setId(UUID.randomUUID().toString());
        user.setActivecode(UUID.randomUUID().toString());
        user.setRegisttime(new Date());
        user.setState((byte) 0);
        
        //注册
        userService.registerUser(user);
        
        model.addAttribute("message", "注册成功,请登录邮箱激活账号!#登录页面#login");
        System.out.println(user.getActivecode());   //暂时不用邮箱转发,直接控制台输出
        return "message";
    }

    @ResponseBody  //pom文件需要添加jackson-core和jackson-databind以转换json
    @PostMapping("/checkUsername")
    public boolean checkUsername(@RequestParam String username) {
        return userService.isUsernameAvail(username);
    }

    @GetMapping("/active/{activecode}")
    public String activeUser(@PathVariable String activecode, Model model) throws UserException {
        userService.active(activecode);
        model.addAttribute("message", "激活成功#登录页面#login");
        return "message";
    }
    
    @ExceptionHandler(UserException.class)
    public ModelAndView error(UserException ue) {
        ModelAndView model = new ModelAndView("message");
        model.addObject("message", ue.getMessage());   //页面跳转通过在message中内嵌然后在jsp中处理,更好的解决方法是将message转化封装为pojo
        return model;
    }
}

UserService.java

package com.bookstore.Service;

import com.bookstore.Dao.UserMapper;
import com.bookstore.Domain.User;
import com.bookstore.Domain.UserExample;
import com.bookstore.Exception.UserException;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
public class UserService {

    @Autowired
    private UserMapper  userDao;

    @Transactional   //声明事务
    public void registerUser(User user) throws UserException {
        //此处选择insertSelective而不是insert是为了让数据库在数据为空时使用默认值
        // insertSelective会自动过滤掉实体对象中值为空的属性,而insert会直接插入null
        int result = userDao.insertSelective(user);
        if(result == 0) throw new UserException("注册失败#注册页面#register");
    }

    public User login(String username, String password) throws UserException {
        UserExample userExample= new UserExample();
        UserExample.Criteria criteria = userExample.createCriteria();
        criteria.andUsernameEqualTo(username).andPasswordEqualTo(password);
        List<User> users = userDao.selectByExample(userExample);
        if(users == null || users.size() !=1 ){
            throw new UserException("用户名或密码不正确#登录页面#login");
        }else if(users.get(0).getState() == 0){
            throw new UserException("账号未激活#登录页面#login");
        }
        return users.get(0);
    }

    //检测用户名是否可用
    public boolean isUsernameAvail(String username){
        UserExample userExample = new UserExample();
        UserExample.Criteria criteria = userExample.createCriteria();
        criteria.andUsernameEqualTo(username);
        List<User> users = userDao.selectByExample(userExample);
        return users.size() == 0 ? true : false;
    }
    
    //用户账号激活
    @Transactional
    public void active(String activecode) throws UserException {
        UserExample userExample = new UserExample();
        UserExample.Criteria criteria = userExample.createCriteria();
        criteria.andActivecodeEqualTo(activecode);
        List<User> users = userDao.selectByExample(userExample);
        if(users != null && users.size()== 1){
            users.get(0).setState((byte)1);
            userDao.updateByExampleSelective(users.get(0), userExample);
        }else{
            throw new UserException("激活失败#-1");
        }
    }
}

/WEB-INF/views/message.jsp 内嵌Java代码处理跳转

<%-- 切割message处理跳转  "信息#跳转页面描述#地址",再利用JS实现跳转 --%>
<%
    String[] linkinfo = ((String) request.getAttribute("message")).split("#");
    String msg = linkinfo[0];
    String[] location = null;
    if(linkinfo.length == 3){
        location = new String[]{linkinfo[1], linkinfo[2]};
    }else if(linkinfo.length == 2){
        location = new String[]{linkinfo[1]};   //设置"-1"以跳转回上一页面
    }
    pageContext.setAttribute("msg", msg);
    pageContext.setAttribute("location", location);
%>

跳转链接

<body class="main" onload="startSecond()">
<c:choose>
		<c:when test='${empty location}'>
            "${pageContext.request.contextPath}/index"
    	c:when>
		<c:when test='${location[0] != "-1"}'>
            "${pageContext.request.contextPath}/${location[1]}"
    	c:when>
		<c:otherwise>"#"c:otherwise>
	c:choose>
><span id="second">5span>秒后自动为您跳转到
	<c:choose>
		<c:when test='${empty location}'>
            <c:out value="首页">c:out>
        c:when>
		<c:when test='${location[0] != "-1"}'>
            <c:out value="${location[0]}">c:out>
        c:when>
		<c:otherwise>
            <c:out value="之前的页面">c:out>
        c:otherwise>
	c:choose>
body>

JS脚本

var interval;

function startSecond() {
	interval = window.setInterval("changeSecond()", 1000);
};

function changeSecond() {
	var second = document.getElementById("second");
	var svalue = second.innerHTML;
	svalue = svalue - 1;
	if (svalue == 0) {
		window.clearInterval(interval);
		var link = document.getElementById("turnhref").getAttribute('href');
		if(link=='#'){
			window.history.go(-1);
		}else{
			location.href=link;
		}
		return;
	}
	second.innerHTML = svalue;
}

四、WEB安全

用户的一切输入都是不可信的,所以需要转义校验,特别是要注意防范SQL注入、XSS、CSRF攻击,小心业务逻辑漏洞。

SpringMVC-showcase中是利用HttpSessionCsrfTokenRepository创建filter来生成csrf_token防范CSRF攻击

你可能感兴趣的:(JAVA,Spring)