在正常的项目开发中,咱们经常须要对程序的参数进行校验来保证程序的安全性。参数校验很是简单,说白了就是对参数进行正确性验证,例如非空验证、范围验证、类型验证等等。校验的方式也有不少种。若是架构设计的比较好的话,可能咱们都不须要作任何验证,或者写比较少的代码就能够知足验证的需求。若是架构设计的有缺陷,或者说压根就没有架构的话,那么咱们对参数进行验证时,就须要咱们写大量相对重复的代码进行验证了。java
下面咱们仍是以上一篇的内容为例,咱们首先手动对参数进行校验。下面为Controller源码:git
package com.jilinwula.springboot.helloworld.controller; import com.jilinwula.springboot.helloworld.Repository.UserInfoRepository; import com.jilinwula.springboot.helloworld.entity.UserInfoEntity; import com.jilinwula.springboot.helloworld.query.UserInfoQuery; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/userinfo") public class UserInfoController { @Autowired private UserInfoRepository userInfoRepository; @GetMapping("/query") public Object list(UserInfoQuery userInfo) { if (StringUtils.isEmpty(userInfo.getUsername())) { return "帐号不能为空"; } if (StringUtils.isEmpty(userInfo.getRoleId()) || userInfo.getRoleId() > 100 || userInfo.getRoleId() < 1) { return "权限不能为空,而且范围为[1-99]"; } UserInfoEntity userInfoEntity = userInfoRepository.findByUsernameAndRoleId(userInfo.getUsername(), userInfo.getRoleId()); return userInfoEntity; } }
咱们只验证了username和roleId参数,分别验证为空验证及范围验证。下面咱们测试一下。启动项目后,访问如下地址:github
http://127.0.0.1:8080/springb...
咱们看一下程序的运行结果。web
由于咱们没有写任何参数,因此参数验证必定是不能经过的。因此就返回的上图中的提示信息。下面咱们看一下数据库中的数据,而后访问一下正确的地址,看看能不能成功的返回数据库中的数据。下图为数据库中的数据:正则表达式
下面咱们访问一下正确的参数,而后看一下返回的结果。访问地址:spring
http://127.0.0.1:8080/springb...
访问结果:数据库
咱们看上图已经成功的返回数据库中的数据了,这就是简单的参数校验,正是由于简单,因此咱们就不作过多的介绍了。下面咱们简单分析一下,这样作参数验证好很差。若是咱们的项目比较简单,那答案必定是确定的,由于站在软件设计角度考虑,不必为了一个简单的功能而设计一个复杂的架构。由于越是复杂的功能,出问题的可能性就越大,程序就越不稳定。但若是站在程序开发角度,那上面的代码必定是有问题的,由于上面的代码根本没办法复用,若是要开发不少这样的项目,要进行参数验证时,那结果必定是代码中有不少相相似的代码,这显然是不合理的。那怎么办呢?那答案就是本篇中的重点内容,也就是SpringBoot对参数的验证,实际上本篇的内容主要是和Spring内容相关和SpringBoot的关系不大。但SpringBoot中基本包括了全部Spring的内容,因此咱们仍是以SpringBoot项目为例。下面咱们看一下,怎么在SpringBoot中的对参数进行校验。json
咱们首先看一下代码,而后在详细介绍代码中的新知识。下面为接受的参数类的源码。浏览器
修改前:安全
package com.jilinwula.springboot.helloworld.query; import lombok.Data; import org.springframework.stereotype.Component; @Component @Data public class UserInfoQuery{ private String username; private Long roleId; }
修改后:
package com.jilinwula.springboot.helloworld.query; import lombok.Data; import org.springframework.stereotype.Component; import javax.validation.constraints.Max; import javax.validation.constraints.Min; import javax.validation.constraints.NotNull; @Component @Data public class UserInfoQuery{ @NotNull(message = "帐号不能为空") private String username; @NotNull(message = "权限不能为空") @Min(value = 1, message = "权限范围为[1-99]") @Max(value = 99, message = "权限范围为[1-99]") private Long roleId; }
咱们看代码中惟一的区别就是添加了不少的注解。没错,在SpringBoot项目中进行参数校验时,就是使用这些注解来完成的。而且注解的命名很直观,基本上经过名字就能够知道什么含义。惟一须要注意的就是这些注解的包是javax中的,而不是其它第三方引入的包。这一点要特别注意,由于不少第三方的包,也包含这些同名的注解。下面咱们继续看Controller中的改动(备注:有关javax中的校验注解相关的使用说明,咱们后续在作介绍)。Controller源码:
改动前:
package com.jilinwula.springboot.helloworld.controller; import com.jilinwula.springboot.helloworld.Repository.UserInfoRepository; import com.jilinwula.springboot.helloworld.entity.UserInfoEntity; import com.jilinwula.springboot.helloworld.query.UserInfoQuery; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/userinfo") public class UserInfoController { @Autowired private UserInfoRepository userInfoRepository; @GetMapping("/query") public Object list(UserInfoQuery userInfo) { if (StringUtils.isEmpty(userInfo.getUsername())) { return "帐号不能为空"; } if (StringUtils.isEmpty(userInfo.getRoleId()) || userInfo.getRoleId() > 100 || userInfo.getRoleId() < 1) { return "权限不能为空,而且范围为[1-99]"; } UserInfoEntity userInfoEntity = userInfoRepository.findByUsernameAndRoleId(userInfo.getUsername(), userInfo.getRoleId()); return userInfoEntity; } }
改动后:
package com.jilinwula.springboot.helloworld.controller; import com.jilinwula.springboot.helloworld.Repository.UserInfoRepository; import com.jilinwula.springboot.helloworld.entity.UserInfoEntity; import com.jilinwula.springboot.helloworld.query.UserInfoQuery; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.BindingResult; import org.springframework.validation.ObjectError; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.validation.Valid; @RestController @RequestMapping("/userinfo") public class UserInfoController { @Autowired private UserInfoRepository userInfoRepository; @GetMapping("/query") public Object list(@Valid UserInfoQuery userInfo, BindingResult result) { if (result.hasErrors()) { for (ObjectError error : result.getAllErrors()) { return error.getDefaultMessage(); } } UserInfoEntity userInfoEntity = userInfoRepository.findByUsernameAndRoleId(userInfo.getUsername(), userInfo.getRoleId()); return userInfoEntity; } }
咱们看代码改动的仍是比较大的首先在入参中添加了@Valid注解。该注解就是标识让SpringBoot对请求参数进行验证。也就是和参数类里的注解是对应的。其次咱们修改了直接在Controller中进行参数判断的逻辑,将之前的代码修改为了SpringBoot中指定的校验方式。下面咱们启动项目,来验证一下上述代码是否能成功的验证参数的正确性。咱们访问下面请求地址:
http://127.0.0.1:8080/springb...
返回结果:
咱们看上图成功的验证了为空的校验,下面咱们试一下范围的验证。咱们访问下面的请求地址:
http://127.0.0.1:8080/springb...
看一下返回结果:
咱们当作功的检测到了参数范围不正确。这就是SpringBoot中的参数验证功能。但上面的代码一个问题,就是只是会返回错误的提示信息,而没有提示,是哪一个参数不正确。下面咱们修改一下代码,来看一下怎么返回是哪一个参数不正确。
package com.jilinwula.springboot.helloworld.controller; import com.jilinwula.springboot.helloworld.Repository.UserInfoRepository; import com.jilinwula.springboot.helloworld.entity.UserInfoEntity; import com.jilinwula.springboot.helloworld.query.UserInfoQuery; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.BindingResult; import org.springframework.validation.FieldError; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.validation.Valid; @RestController @RequestMapping("/userinfo") public class UserInfoController { @Autowired private UserInfoRepository userInfoRepository; @GetMapping("/query") public Object list(@Valid UserInfoQuery userInfo, BindingResult result) { if (result.hasErrors()) { FieldError error = result.getFieldError(); return error.getField() + "+" + error.getDefaultMessage(); } UserInfoEntity userInfoEntity = userInfoRepository.findByUsernameAndRoleId(userInfo.getUsername(), userInfo.getRoleId()); return userInfoEntity; } }
咱们将获取ObjectError的类型修改为了FieldError。由于FieldError类型能够获取到验证错误的字段名字,因此咱们将ObjectError修改成FieldError。下面咱们看一下请求返回的结果。
咱们看这回咱们就获取到了验证错误的字段名子了。在实际的项目开发中,咱们在返回接口数据时,大部分都会采用json格式的方式返回,下面咱们简单封装一个返回的类,使上面的验证返回json格式。下面为封装的返回类的源码:
package com.jilinwula.springboot.helloworld.utils; import lombok.Data; @Data public class Return { private int code; private Object data; private String msg; public static Return error(Object data, String msg) { Return r = new Return(); r.setCode(-1); r.setData(data); r.setMsg(msg); return r; } }
Controller修改:
package com.jilinwula.springboot.helloworld.controller; import com.jilinwula.springboot.helloworld.Repository.UserInfoRepository; import com.jilinwula.springboot.helloworld.entity.UserInfoEntity; import com.jilinwula.springboot.helloworld.query.UserInfoQuery; import com.jilinwula.springboot.helloworld.utils.Return; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.BindingResult; import org.springframework.validation.FieldError; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.validation.Valid; @RestController @RequestMapping("/userinfo") public class UserInfoController { @Autowired private UserInfoRepository userInfoRepository; @GetMapping("/query") public Object list(@Valid UserInfoQuery userInfo, BindingResult result) { if (result.hasErrors()) { FieldError error = result.getFieldError(); return Return.error(error.getField(), error.getDefaultMessage()); } UserInfoEntity userInfoEntity = userInfoRepository.findByUsernameAndRoleId(userInfo.getUsername(), userInfo.getRoleId()); return userInfoEntity; } }
咱们仍是启动项目,并访问下面地址看看返回的结果:
http://127.0.0.1:8080/springb...
返回结果:
这样咱们就返回一个简单的json类型的数据了。虽然咱们的校验参数的逻辑没有在Controller里面写,但咱们仍是在Controller里面写了不少和业务无关的代码,而且这些代码仍是重复的,这显然是不合理的。咱们能够将上述相同的代码的封装起来,而后统一的处理。这样就避免了有不少重复的代码了。那这代码封装到哪里呢?咱们可使用Spring中的切面功能。由于SpringBoot中基本包括了全部Spring中的技术,因此,咱们能够放心大胆的在SpringBoot项目中使用Spring中的技术。咱们知道在使用切面技术时,咱们能够对方法进行前置加强、后置加强、环绕加强等。这样咱们就能够利用切面的技术,在方法以前,也就是请求Controller以前,作参数的校验工做,这样就不会对咱们的业务代码产生侵入了。下面咱们看一下切面的源码而后在作详细说明:
package com.jilinwula.springboot.helloworld.aspect; import com.jilinwula.springboot.helloworld.utils.Return; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.springframework.stereotype.Component; import org.springframework.validation.BindingResult; import org.springframework.validation.FieldError; @Slf4j @Aspect @Component public class UserAspect { @Before("execution(public * com.jilinwula.springboot.helloworld.controller..*(..))") public void doBefore(JoinPoint joinPoint) { for (Object arg : joinPoint.getArgs()) { if (arg instanceof BindingResult) { BindingResult result = (BindingResult) arg; if (result.hasErrors()) { FieldError error = result.getFieldError(); Return.error(error.getField(), error.getDefaultMessage()); } } } } }
咱们看上述的代码中咱们添加了一个@Aspect注解,这个就是切面的注解,而后咱们在方法中又添加了@Before注解,也就是对目标方法进行前置加强,Spring在请求Controller以前会先请求此方法。因此咱们能够将校验参数的代码逻辑写在这个方法中。execution参数为切点函数,也就是目标方法的切入点。切点函数包含一些通配符的语法,下面咱们简单介绍一下:
咱们经过上述代码知道,Spring中的切面功能是没有返回值的。因此咱们在使用切面功能时,是没有办法在切面里面作参数返回的。那咱们应该怎么办呢?这时异常就派上用场了。咱们知道当程序抛出异常时,若是当前方法没有作try catch处理,那么异常就会一直向上抛出,若是程序也一直没有作处理,那么当前异常就会一直抛出,直到被Java虚拟机捕获。但Java虚拟机也不会对异常进行处理,而是直接抛出异常。这也就是程序不作任何处理抛出异常的根本缘由。咱们正好能够利用异常的这种特性,返回参数验证的结果。由于在Spring中为咱们提供了统一捕获异常的方法,咱们能够在这个方法中,将咱们的异常信息封装成json格式,这样咱们就能够返回统一的jons格式了。因此在上述的切面中咱们手动了抛出了一个异常。该异常由于咱们没有用任何处理,因此上述异常会被SpringBoot中的统一异常拦截处理。这样当SpringBoot检测到参数不正确时,就会抛出一个异常,而后SpringBoot就会检测到程序抛出的异常,而后返回异常中的信息。下面咱们看一下异常类的源码:
异常类:
package com.jilinwula.springboot.helloworld.exception; import com.jilinwula.springboot.helloworld.utils.Return; import lombok.Data; @Data public class UserInfoException extends RuntimeException { private Return r; public UserInfoException(Return r) { this.r = r; } }
Return源码:
package com.jilinwula.springboot.helloworld.utils; import com.jilinwula.springboot.helloworld.exception.UserInfoException; import lombok.Data; @Data public class Return { private int code; private Object data; private String msg; public static void error(Object data, String msg) { Return r = new Return(); r.setCode(-1); r.setData(data); r.setMsg(msg); throw new UserInfoException(r); } public static Return success() { Return r = new Return(); r.setCode(0); return r; } }
由于该异常类比较简单,咱们就不会过多的介绍了,惟一有一点须要注意的是该异常类继承的是RuntimeException异常类,而不是Exception异常类,缘由咱们已经在上一篇中介绍了,Spring只会回滚RuntimeException异常类及其子类,而不会回滚Exception异常类的。下面咱们看一下Spring中统一拦截异常处理,下面为该类的源码:
package com.jilinwula.springboot.helloworld.handler; import com.jilinwula.springboot.helloworld.exception.UserInfoException; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; @Slf4j @RestControllerAdvice public class UserInfoHandler { /** * 校验错误拦截处理 * * @param e 错误信息集合 * @return 错误信息 */ @ExceptionHandler(UserInfoException.class) public Object handle(UserInfoException e) { return e.getR(); } }
咱们在该类添加了@RestControllerAdvice注解。该注解就是为了定义咱们统一获取异常拦截的。而后咱们又添加了@ExceptionHandler注解,该注解就是用来拦截异常类的注解,而且能够在当前方法中,直接获取到该异常类的对象信息。这样咱们直接返回这个异常类的信息就能够了。由于咱们在这个自定义异常类中添加了Return参数,因此,咱们只要反悔Return对象的信息便可,而不用返回整个异常的信息。下面咱们访问一下下面的请求,看看上述代码是否能检测到参数不正确。请求地址:
http://127.0.0.1:8080/springb...
返回结果:
这样咱们完成了参数校验的功能了,而且这种方式有很大的复用性,即便咱们在写新的Controller,也不须要手动的校验参数了,只要咱们的请求参数是UserInfoQuery类就能够了。还有一点要注意,因此咱们不用手动验证参数了,但咱们的请求参数中仍是要写BindingResult参数,这一点要特别注意。
下面咱们更详细的介绍一下参数验证的注解,咱们首先看一下正则校验,咱们在实体类中添加一个新属性,而后用正则的的方式,验证该参数的正确性。下面为实体类源码:
package com.jilinwula.springboot.helloworld.query; import lombok.Data; import org.springframework.stereotype.Component; import javax.validation.constraints.Max; import javax.validation.constraints.Min; import javax.validation.constraints.NotNull; import javax.validation.constraints.Pattern; @Component @Data public class UserInfoQuery{ @NotNull(message = "用户编号不能为空") @Pattern(regexp = "^[1-10]$",message = "用户编号范围不正确") private String id; @NotNull(message = "帐号不能为空") private String username; @NotNull(message = "权限不能为空") @Min(value = 1, message = "权限范围为[1-99]") @Max(value = 99, message = "权限范围为[1-99]") private Long roleId; }
下面咱们访问如下地址:
http://127.0.0.1:8080/springb...
但这回咱们不在浏览器里请求,由于浏览器请求不太方便,而且返回的json格式也没有格式化不方便浏览,除非要装一些浏览器插件才能够。实际上在IDEA中咱们能够很方便的请求一下接口地址,而且返回的json内容是自动格式化的。下面咱们来看一下怎么在IDEA中发起接口请求。在IDEA中请求一个接口很简单,咱们只要建立一个.http类型的文件名字就能够。而后咱们能够在该文件中,指定咱们接口的请求类型,例如GET或者POST。当咱们在文件的开口写GET或者POST时,IDEA会自动有相应的提示。下面咱们看一下http文件中的内容。
http.http:
GET http://127.0.0.1:8080/springboot/userinfo/query?roleId=3&username=阿里巴巴&id=-1
这时标识GET参数的地方,就会出现绿色剪头,但咱们点击这个绿色箭头,IDEA就会就会启动请求GET参数后面的接口。下面咱们看一下上述的返回结果。
GET http://127.0.0.1:8080/springboot/userinfo/query?roleId=3&username=%E9%98%BF%E9%87%8C%E5%B7%B4%E5%B7%B4&id=-1 HTTP/1.1 200 Content-Type: application/json;charset=UTF-8 Transfer-Encoding: chunked Date: Mon, 18 Feb 2019 03:57:29 GMT { "code": -1, "data": "id", "msg": "用户编号范围不正确" } Response code: 200; Time: 24ms; Content length: 41 bytes
这就是.http文件类型的返回结果,用该文件请求接口,相比用浏览器来讲,要方便的多。由于咱们在实体类中使用正则指定参数范围为1-10,因此请求接口时反悔了id参数有错误。下面咱们输入一个正确的值在看一下返回结果。
http.http:
GET http://127.0.0.1:8080/springboot/userinfo/query?roleId=3&username=阿里巴巴&id=1
返回结果:
GET <http://127.0.0.1:8080/springboot/userinfo/query?roleId=3&username=%E9%98%BF%E9%87%8C%E5%B7%B4%E5%B7%B4&id=1> HTTP/1.1 200 Content-Type: application/json;charset=UTF-8 Transfer-Encoding: chunked Date: Mon, 18 Feb 2019 05:46:49 GMT { "id": 61, "username": "阿里巴巴", "password": "alibaba", "nickname": "阿里巴巴", "roleId": 3 } Response code: 200; Time: 25ms; Content length: 77 bytes
咱们看已经正确的返回数据库中的数据了。在Spring中,提供了不少种注解来方便咱们进行参数校验,下面是比较常见的注解:
注解 | 做用 | |
---|---|---|
@Null | 参数必须为null | |
@NotNull | 参数必须不为null | |
@NotBlank | 参数必须不为null,而且长度必须大于0 | |
@NotEmpty | 参数必须不为空 | |
@Min | 参数必须大于等于该值 | |
@Max | 参数必须小于等于该值 | |
@Size | 参数必须在指定的范围内 | |
@Past | 参数必须是一个过时的时间 | |
@Future | 参数必须是一个将来的时间 | |
@Pattern | 参数必须知足正则表达式 | |
参数必须为电子邮箱 |
上述内容就是SpringBoot中的参数校验所有内容,若有不正确的欢迎留言,谢谢。
https://github.com/jilinwula/...
http://jilinwula.com/article/...