在上一章第三十六章:基于SpringBoot架构重写SpringMVC请求参数装载中我们说到了怎么去重写SpringMVC
参数装载,从而来完成我们的需求。本章内容会在上一章的基础上进行修改!
企业中接口编写是再频繁不过的事情了,现在接口已经不仅仅用于移动端来做数据服务了,一些管理平台也同样采用了这种方式来完成前后完全分离的模式。不管是接口也好、分离模式也好都会涉及到数据安全的问题,那我们怎么可以很好的避免我们的数据参数暴露呢?
本章目标
基于SpringBoot平台实现参数安全传输。
SpringBoot 企业级核心技术学习专题
专题 | 专题名称 | 专题描述 |
---|---|---|
001 | Spring Boot 核心技术 | 讲解SpringBoot一些企业级层面的核心组件 |
002 | Spring Boot 核心技术章节源码 | Spring Boot 核心技术简书每一篇文章码云对应源码 |
003 | Spring Cloud 核心技术 | 对Spring Cloud核心技术全面讲解 |
004 | Spring Cloud 核心技术章节源码 | Spring Cloud 核心技术简书每一篇文章对应源码 |
005 | QueryDSL 核心技术 | 全面讲解QueryDSL核心技术以及基于SpringBoot整合SpringDataJPA |
006 | SpringDataJPA 核心技术 | 全面讲解SpringDataJPA核心技术 |
007 | SpringBoot核心技术学习目录 | SpringBoot系统的学习目录,敬请关注点赞!!! |
构建项目
本章所需要的依赖比较少,我们添加相应的Web依赖即可,下面是pom.xml配置文件部分依赖内容:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<!--<scope>provided</scope>-->
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<!--<scope>test</scope>-->
</dependency>
<!--fastjson支持-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.38</version>
</dependency>
</dependencies>
本章的实现思路是采用SpringMvc拦截器
来完成指定注解的拦截,并且根据拦截做出安全属性的处理,再结合自定义的参数装载完成对应参数的赋值。
ContentSecurityMethodArgumentResolver
我们先来创建一个参数装载实现类,该参数状态实现类继承至BaseMethodArgumentResolver
,而BaseMethodArgumentResolver
则是实现了HandlerMethodArgumentResolver
接口完成一些父类的方法处理,代码如下所示:
package com.yuqiyu.chapter37.resovler;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.HandlerMapping;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
/**
* ===============================
* Created with IntelliJ IDEA.
* User:于起宇
* Date:2017/8/23
* Time:20:04
* 简书:http://www.jianshu.com/u/092df3f77bca
* ================================
*/
public abstract class BaseMethodArgumentResolver
implements HandlerMethodArgumentResolver
{
/**
* 获取指定前缀的参数:包括uri varaibles 和 parameters
*
* @param namePrefix
* @param request
* @return
* @subPrefix 是否截取掉namePrefix的前缀
*/
protected Map<String, String[]> getPrefixParameterMap(String namePrefix, NativeWebRequest request, boolean subPrefix) {
Map<String, String[]> result = new HashMap();
Map<String, String> variables = getUriTemplateVariables(request);
int namePrefixLength = namePrefix.length();
for (String name : variables.keySet()) {
if (name.startsWith(namePrefix)) {
//page.pn 则截取 pn
if (subPrefix) {
char ch = name.charAt(namePrefix.length());
//如果下一个字符不是 数字 . _ 则不可能是查询 只是前缀类似
if (illegalChar(ch)) {
continue;
}
result.put(name.substring(namePrefixLength + 1), new String[]{variables.get(name)});
} else {
result.put(name, new String[]{variables.get(name)});
}
}
}
Iterator<String> parameterNames = request.getParameterNames();
while (parameterNames.hasNext()) {
String name = parameterNames.next();
if (name.startsWith(namePrefix)) {
//page.pn 则截取 pn
if (subPrefix) {
char ch = name.charAt(namePrefix.length());
//如果下一个字符不是 数字 . _ 则不可能是查询 只是前缀类似
if (illegalChar(ch)) {
continue;
}
result.put(name.substring(namePrefixLength + 1), request.getParameterValues(name));
} else {
result.put(name, request.getParameterValues(name));
}
}
}
return result;
}
private boolean illegalChar(char ch) {
return ch != '.' && ch != '_' && !(ch >= '0' && ch <= '9');
}
@SuppressWarnings("unchecked")
protected final Map<String, String> getUriTemplateVariables(NativeWebRequest request) {
Map<String, String> variables =
(Map<String, String>) request.getAttribute(
HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST);
return (variables != null) ? variables : Collections.<String, String>emptyMap();
}
}
下面我们主要来看看ContentSecurityMethodArgumentResolver
编码与我们上一章第三十六章:基于SpringBoot架构重写SpringMVC请求参数装载有什么区别,实现思路几乎是一样的,只是做部分内容做出了修改,代码如下所示:
package com.yuqiyu.chapter37.resovler;
import com.yuqiyu.chapter37.annotation.ContentSecurityAttribute;
import com.yuqiyu.chapter37.constants.ContentSecurityConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.util.StringUtils;
import org.springframework.validation.BindException;
import org.springframework.validation.DataBinder;
import org.springframework.validation.Errors;
import org.springframework.web.bind.ServletRequestDataBinder;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.ModelAndViewContainer;
import javax.servlet.http.HttpServletRequest;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.Enumeration;
import java.util.Map;
/**
* 自定义方法参数映射
* 实现了HandlerMethodArgumentResolver接口内的方法supportsParameter & resolveArgument
* 通过supportsParameter方法判断仅存在@ContentSecurityAttribute注解的参数才会执行resolveArgument方法实现
* ===============================
* Created with IntelliJ IDEA.
* User:于起宇
* Date:2017/10/11
* Time:23:05
* 简书:http://www.jianshu.com/u/092df3f77bca
* ================================
*/
public class ContentSecurityMethodArgumentResolver
extends BaseMethodArgumentResolver
{
private Logger logger = LoggerFactory.getLogger(ContentSecurityMethodArgumentResolver.class);
/**
* 判断参数是否配置了@ContentSecurityAttribute注解
* 如果返回true则执行resolveArgument方法
* @param parameter
* @return
*/
@Override
public boolean supportsParameter(MethodParameter parameter)
{
return parameter.hasParameterAnnotation(ContentSecurityAttribute.class);
}
/**
* 执行参数映射
* @param parameter 参数对象
* @param mavContainer 参数集合
* @param request 本地请求对象
* @param binderFactory 绑定参数工厂对象
* @return
* @throws Exception
*/
@Override
public Object resolveArgument(
MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest request,
WebDataBinderFactory binderFactory) throws Exception
{
//获取@ContentSecurityAttribute配置的value值,作为参数名称
String name = parameter.getParameterAnnotation(ContentSecurityAttribute.class).value();
/**
* 获取值
* 如果请求集合内存在则直接获取
* 如果不存在则调用createAttribute方法创建
*/
Object target = (mavContainer.containsAttribute(name)) ?
mavContainer.getModel().get(name) : createAttribute(name, parameter, binderFactory, request);
/**
* 创建参数绑定
*/
WebDataBinder binder = binderFactory.createBinder(request, target, name);
//获取返回值实例
target = binder.getTarget();
//如果存在返回值
if (target != null) {
/**
* 设置返回值对象内的所有field得值,从request.getAttribute方法内获取
*/
bindRequestAttributes(binder, request);
/**
* 调用@Valid验证参数有效性
*/
validateIfApplicable(binder, parameter);
/**
* 存在参数绑定异常
* 抛出异常
*/
if (binder.getBindingResult().hasErrors()) {
if (isBindExceptionRequired(binder, parameter)) {
throw new BindException(binder.getBindingResult());
}
}
}
/**
* 转换返回对象
*/
target = binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType());
//存放到model内
mavContainer.addAttribute(name, target);
return target;
}
/**
* 绑定请求参数
* @param binder
* @param nativeWebRequest
* @throws Exception
*/
protected void bindRequestAttributes(
WebDataBinder binder,
NativeWebRequest nativeWebRequest) throws Exception {
/**
* 获取返回对象实例
*/
Object obj = binder.getTarget();
/**
* 获取返回值类型
*/
Class<?> targetType = binder.getTarget().getClass();
/**
* 转换本地request对象为HttpServletRequest对象
*/
HttpServletRequest request =
nativeWebRequest.getNativeRequest(HttpServletRequest.class);
/**
* 获取所有attributes
*/
Enumeration attributeNames = request.getAttributeNames();
/**
* 遍历设置值
*/
while(attributeNames.hasMoreElements())
{
//获取attribute name
String attributeName = String.valueOf(attributeNames.nextElement());
/**
* 仅处理ContentSecurityConstants.ATTRIBUTE_PREFFIX开头的attribute
*/
if(!attributeName.startsWith(ContentSecurityConstants.ATTRIBUTE_PREFFIX))
{
continue;
}
//获取字段名
String fieldName = attributeName.replace(ContentSecurityConstants.ATTRIBUTE_PREFFIX,"");
Field field = null;
try {
field = targetType.getDeclaredField(fieldName);
}
/**
* 如果返回对象类型内不存在字段
* 则从父类读取
*/
catch (NoSuchFieldException e)
{
try {
field = targetType.getSuperclass().getDeclaredField(fieldName);
}catch (NoSuchFieldException e2)
{
continue;
}
/**
* 如果父类还不存在,则直接跳出循环
*/
if(StringUtils.isEmpty(field)) {
continue;
}
}
/**
* 设置字段的值
*/
field.setAccessible(true);
String fieldClassName = field.getType().getSimpleName();
Object attributeObj = request.getAttribute(attributeName);
logger.info("映射安全字段:{},字段类型:{},字段内容:{}",fieldName,fieldClassName,attributeObj);
if("String".equals(fieldClassName)) {
field.set(obj,attributeObj);
}
else if("Integer".equals(fieldClassName))
{
field.setInt(obj,Integer.valueOf(String.valueOf(attributeObj)));
}
else{
field.set(obj,attributeObj);
}
}
ServletRequestDataBinder servletBinder = (ServletRequestDataBinder) binder;
servletBinder.bind(new MockHttpServletRequest());
}
/**
* Whether to raise a {@link BindException} on bind or validation errors.
* The default implementation returns {@code true} if the next method
* argument is not of type {@link Errors}.
*
* @param binder the data binder used to perform data binding
* @param parameter the method argument
*/
protected boolean isBindExceptionRequired(WebDataBinder binder, MethodParameter parameter) {
int i = parameter.getParameterIndex();
Class<?>[] paramTypes = parameter.getMethod().getParameterTypes();
boolean hasBindingResult = (paramTypes.length > (i + 1) && Errors.class.isAssignableFrom(paramTypes[i + 1]));
return !hasBindingResult;
}
/**
* Extension point to create the model attribute if not found in the model.
* The default implementation uses the default constructor.
*
* @param attributeName the name of the attribute, never {@code null}
* @param parameter the method parameter
* @param binderFactory for creating WebDataBinder instance
* @param request the current request
* @return the created model attribute, never {@code null}
*/
protected Object createAttribute(String attributeName, MethodParameter parameter,
WebDataBinderFactory binderFactory, NativeWebRequest request) throws Exception {
String value = getRequestValueForAttribute(attributeName, request);
if (value != null) {
Object attribute = createAttributeFromRequestValue(value, attributeName, parameter, binderFactory, request);
if (attribute != null) {
return attribute;
}
}
return BeanUtils.instantiateClass(parameter.getParameterType());
}
/**
* Obtain a value from the request that may be used to instantiate the
* model attribute through type conversion from String to the target type.
* <p>The default implementation looks for the attribute name to match
* a URI variable first and then a request parameter.
*
* @param attributeName the model attribute name
* @param request the current request
* @return the request value to try to convert or {@code null}
*/
protected String getRequestValueForAttribute(String attributeName, NativeWebRequest request) {
Map<String, String> variables = getUriTemplateVariables(request);
if (StringUtils.hasText(variables.get(attributeName))) {
return variables.get(attributeName);
} else if (StringUtils.hasText(request.getParameter(attributeName))) {
return request.getParameter(attributeName);
} else {
return null;
}
}
/**
* Create a model attribute from a String request value (e.g. URI template
* variable, request parameter) using type conversion.
* <p>The default implementation converts only if there a registered
* {@link org.springframework.core.convert.converter.Converter} that can perform the conversion.
*
* @param sourceValue the source value to create the model attribute from
* @param attributeName the name of the attribute, never {@code null}
* @param parameter the method parameter
* @param binderFactory for creating WebDataBinder instance
* @param request the current request
* @return the created model attribute, or {@code null}
* @throws Exception
*/
protected Object createAttributeFromRequestValue(String sourceValue,
String attributeName,
MethodParameter parameter,
WebDataBinderFactory binderFactory,
NativeWebRequest request) throws Exception {
DataBinder binder = binderFactory.createBinder(request, null, attributeName);
ConversionService conversionService = binder.getConversionService();
if (conversionService != null) {
TypeDescriptor source = TypeDescriptor.valueOf(String.class);
TypeDescriptor target = new TypeDescriptor(parameter);
if (conversionService.canConvert(source, target)) {
return binder.convertIfNecessary(sourceValue, parameter.getParameterType(), parameter);
}
}
return null;
}
/**
* Validate the model attribute if applicable.
* <p>The default implementation checks for {@code @javax.validation.Valid}.
*
* @param binder the DataBinder to be used
* @param parameter the method parameter
*/
protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
Annotation[] annotations = parameter.getParameterAnnotations();
for (Annotation annot : annotations) {
if (annot.annotationType().getSimpleName().startsWith("Valid")) {
Object hints = AnnotationUtils.getValue(annot);
binder.validate(hints instanceof Object[] ? (Object[]) hints : new Object[]{hints});
}
}
}
}
ContentSecurityAttribute
可以看到supportsParameter
方法我们是完成了参数包含ContentSecurityAttribute
注解才会做装载处理,也就是说只要参数配置了ContentSecurityAttribute
注解才会去执行resolveArgument
方法内的业务逻辑并做出相应的返回。注解内容如下所示:
package com.yuqiyu.chapter37.annotation;
import java.lang.annotation.*;
/**
* 配置该注解表示从request.attribute内读取对应实体参数值
* ========================
* Created with IntelliJ IDEA.
* User:恒宇少年
* Date:2017/10/11
* Time:23:02
* 码云:http://git.oschina.net/jnyqy
* ========================
*/
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ContentSecurityAttribute {
/**
* 参数值
* 对应配置@ContentSecurityAttribute注解的参数名称即可
* @return
*/
String value();
}
在上面注解代码内我们添加了一个属性value
,这个属性是配置的参数的映射名称,其实目的跟@RequestParam
有几分相似,在我们配置使用的时候保持value与参数名称一致就可以了。
接下来我们还需要创建一个注解,因为我们不希望所有的请求都被做出处理!
ContentSecurity
该注解配置在控制器内的方法上,只要配置了该注解就会被处理一些安全机制,我们先来看看该注解的代码,至于具体怎么使用以及内部做出了什么安全机制,一会我们再来详细讲解,代码如下:
package com.yuqiyu.chapter37.annotation;
import com.yuqiyu.chapter37.enums.ContentSecurityAway;
import java.lang.annotation.*;
/**
* 配置开启安全
* ========================
* Created with IntelliJ IDEA.
* User:恒宇少年
* Date:2017/10/11
* Time:22:55
* 码云:http://git.oschina.net/jnyqy
* ========================
*/
@Target({ ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ContentSecurity
{
/**
* 内容加密方式
* 默认DES
* @return
*/
ContentSecurityAway away() default ContentSecurityAway.DES;
}
在注解内我们添加了away
属性方法,而该属性方法我们采用了一个枚举的方式完成,我们先来看看枚举的值再来说下作用,如下所示:
package com.yuqiyu.chapter37.enums;
/**
* 内容安全处理方式
* 目前可配置:DES
* 可扩展RSA、JWT、OAuth2等
* ========================
* Created with IntelliJ IDEA.
* User:恒宇少年
* Date:2017/10/11
* Time:22:55
* 码云:http://git.oschina.net/jnyqy
* ========================
*/
public enum ContentSecurityAway {
DES
}
可以看到ContentSecurityAway
内目前我们仅声明了一个类型DES
,其实这个枚举创建是为了以后的扩展,如果说以后我们的加密方式会存在多种,只需要在ContentSecurityAway
添加对应的配置,以及处理安全机制部分做出调整,其他部分不需要做出任何修改。
那现在我们可以说万事俱备就差处理安全机制了,在文章的开头有说到,我们需要采用拦截器来完成安全的认证,那么我们接下来看看拦截器的实现。
ContentSecurityInterceptor
ContentSecurityInterceptor
拦截器实现HandlerInterceptor
接口,并且需要我们重写内部的三个方法,分别是preHandle
、postHandle
、afterCompletion
,我们本章其实只需要将安全认证处理编写在preHandle
方法内,因为我们需要在请求Controller
之前做出认证,下面我们还是先把代码贴出来,如下所示:
package com.yuqiyu.chapter37.interceptor;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.yuqiyu.chapter37.annotation.ContentSecurity;
import com.yuqiyu.chapter37.constants.ContentSecurityConstants;
import com.yuqiyu.chapter37.utils.DES3Util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.util.Iterator;
/**
* 安全认证拦截器
* ========================
* Created with IntelliJ IDEA.
* User:恒宇少年
* Date:2017/10/11
* Time:22:53
* 码云:http://git.oschina.net/jnyqy
* ========================
*/
public class ContentSecurityInterceptor
implements HandlerInterceptor
{
/**
* logback
*/
private static Logger logger = LoggerFactory.getLogger(ContentSecurityInterceptor.class);
/**
* 请求之前处理加密内容
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//默认可以通过
boolean isPass = true;
/**
* 获取请求映射方法对象
*/
HandlerMethod handlerMethod = (HandlerMethod) handler;
/**
* 获取访问方法实例对象
*/
Method method = handlerMethod.getMethod();
/**
* 检查是否存在内容安全验证注解
*/
ContentSecurity security = method.getAnnotation(ContentSecurity.class);
/**
* 存在注解做出不同方式认证处理
*/
if (security != null) {
switch (security.away())
{
//DES方式内容加密处理
case DES:
isPass = checkDES(request,response);
break;
}
}
return isPass;
}
@Override
public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
}
/**
* 检查DES方式内容
* @param request
* @param response
* @return
*/
boolean checkDES(HttpServletRequest request,HttpServletResponse response) throws Exception
{
//获取desString加密内容
String des = request.getParameter(ContentSecurityConstants.DES_PARAMETER_NAME);
logger.info("请求加密参数内容:{}",des);
/**
* 加密串不存在
*/
if (des == null || des.length() == 0) {
JSONObject json = new JSONObject();
json.put("msg","The DES Content Security Away Request , Parameter Required is "+ ContentSecurityConstants.DES_PARAMETER_NAME);
response.getWriter().print(JSON.toJSONString(json));
return false;
}
/**
* 存在加密串
* 解密DES参数列表并重新添加到request内
*/
try {
des = DES3Util.decrypt(des, DES3Util.DESKEY,"UTF-8");
if (!StringUtils.isEmpty(des)) {
JSONObject params = JSON.parseObject(des);
logger.info("解密请求后获得参数列表 >>> {}", des);
Iterator it = params.keySet().iterator();
while (it.hasNext()) {
/**
* 获取请求参数名称
*/
String parameterName = it.next().toString();
/**
* 参数名称不为空时将值设置到request对象内
* key=>value
*/
if (!StringUtils.isEmpty(parameterName)) {
request.setAttribute(ContentSecurityConstants.ATTRIBUTE_PREFFIX + parameterName,params.get(parameterName));
}
}
}
}catch (Exception e)
{
logger.error(e.getMessage());
JSONObject json = new JSONObject();
json.put("msg","The DES Content Security Error."+ContentSecurityConstants.DES_PARAMETER_NAME);
response.getWriter().print(JSON.toJSONString(json));
return false;
}
return true;
}
}
在上面的代码preHandle
方法中,拦截器首先判断当前请求方法是否包含ContentSecurity
自定义安全注解,如果存在则是证明了该方法需要我们做安全解密,客户端传递参数的时候应该是已经按照预先定于的规则进行加密处理的。
接下来就是根据配置的加密方式进行ContentSecurityAway
枚举类型switch case
选择,根据不同的配置执行不同的解密方法。
因为我们的ContentSecurityAway`注解内仅配置了
DES方式,我们就来看看
checkDES``方法是怎么与客户端传递参数的约定,当然这个约定这里只是一个示例,如果你的项目需要更复杂的加密形式直接进行修改就可以了。
上面代码中最主要的一部分则是,如下所示:
...省略部分代码
des = DES3Util.decrypt(des, DES3Util.DESKEY,"UTF-8");
if (!StringUtils.isEmpty(des)) {
JSONObject params = JSON.parseObject(des);
logger.info("解密请求后获得参数列表 >>> {}", des);
Iterator it = params.keySet().iterator();
while (it.hasNext()) {
/**
* 获取请求参数名称
*/
String parameterName = it.next().toString();
/**
* 参数名称不为空时将值设置到request对象内
* key=>value
*/
if (!StringUtils.isEmpty(parameterName)) {
request.setAttribute(ContentSecurityConstants.ATTRIBUTE_PREFFIX + parameterName,params.get(parameterName));
}
}
}
....省略部分代码
在平时,客户端发起请求时参数都是在
HttpServletRequest
对象的Parameter
内,如果我们做出解密后是无法再次将参数存放到Parameter
内的,因为不可修改,HttpServletRequest
不允许让这么处理参数,也是防止请求参数被篡改!
既然这种方式不可以,那么我就采用Attribute
方式设置,将加密字符串解密完成获取相应参数后,将每一个参数设置的Attribute
请求属性集合内,这里你可能会有一个疑问,我们什么时候获取Attribute
的值呢?
其实在上面代码ContentSecurityMethodArgumentResolver
类内的方法bindRequestAttributes
内,我们就已经从Attribute
获取所有的属性列表,然后通过反射机制
设置到配置ContentSecurityAttribute
安全注解属性的参数对象内,然而我们这种方式目前是仅仅支持实体类
,而基本数据封装类型目前没有做处理。
这样在处理完成反射对象设置对应字段的属性后。然后通过
resolveArgument
方法将参数对象实例返回就完成了参数的自定义装载过程。
处理参数数据验证
我们既然自定义了参数装载,当然不能忘记处理参数的验证机制,这也是Spring MVC
引以为傲的功能模块之一,Spring MVC Validator
其实是采用了Hibernate Validator
机制完成的数据验证,我们只需要判断参数是否存在@Valid
注解是否存在,如果存在则去执行WebDataBinder
的validate
方法就可以完成数据有效性验证,相关代码如下所示:
/**
* Validate the model attribute if applicable.
* <p>The default implementation checks for {@code @javax.validation.Valid}.
*
* @param binder the DataBinder to be used
* @param parameter the method parameter
*/
protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
Annotation[] annotations = parameter.getParameterAnnotations();
for (Annotation annot : annotations) {
if (annot.annotationType().getSimpleName().startsWith("Valid")) {
Object hints = AnnotationUtils.getValue(annot);
binder.validate(hints instanceof Object[] ? (Object[]) hints : new Object[]{hints});
}
}
}
上述代码同样是位于ContentSecurityMethodArgumentResolver
参数装载类内,到目前为止我们的参数状态从拦截 > 验证 > 装载
一整个过程已经编写完成,下面我们配置下相关的拦截器以及安全参数装载让SpringBoot
框架支持。
WebMvcConfiguration
先把拦截器进行配置下,代码如下所示:
package com.yuqiyu.chapter37;
import com.yuqiyu.chapter37.interceptor.ContentSecurityInterceptor;
import com.yuqiyu.chapter37.resovler.ContentSecurityMethodArgumentResolver;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import java.util.List;
/**
* springmvc 注解式配置类
* ========================
* Created with IntelliJ IDEA.
* User:恒宇少年
* Date:2017/9/16
* Time:22:15
* 码云:http://git.oschina.net/jnyqy
* ========================
*/
@Configuration
public class WebMvcConfiguration
extends WebMvcConfigurerAdapter
{
/**
* 配置拦截器
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new ContentSecurityInterceptor()).addPathPatterns("/**");
}
}
我们配置安全拦截器拦截所有/**
根下的请求。下面配置下参数装载,在WebMvcConfigurerAdapter
抽象类内有一个方法addArgumentResolvers
就可以完成自定义参数装载配置,代码如下所示:
/**
* 添加参数装载
* @param argumentResolvers
*/
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
/**
* 将自定义的参数装载添加到spring内托管
*/
argumentResolvers.add(new ContentSecurityMethodArgumentResolver());
}
就这么简单了就配置完成了。
测试安全请求
添加测试实体
测试实体代码如下所示:
package com.yuqiyu.chapter37.bean;
import lombok.Data;
import org.hibernate.validator.constraints.NotEmpty;
import javax.validation.constraints.Min;
/**
* ========================
* Created with IntelliJ IDEA.
* User:恒宇少年
* Date:2017/10/14
* Time:10:41
* 码云:http://git.oschina.net/jnyqy
* ========================
*/
@Data
public class StudentEntity {
//学生姓名
@NotEmpty
private String name;
//年龄
@Min(value = 18,message = "年龄最小18岁")
private int age;
}
在上述测试实体类内我们添加了两个属性,name
、age
,其中都做了验证注解配置,那我们下面就针对该实体添加一个控制器方法来进行测试安全参数装载。
测试控制器
创建一个IndexController
控制器,具体代码如下所示:
package com.yuqiyu.chapter37.controller;
import com.alibaba.fastjson.JSON;
import com.yuqiyu.chapter37.annotation.ContentSecurity;
import com.yuqiyu.chapter37.annotation.ContentSecurityAttribute;
import com.yuqiyu.chapter37.bean.StudentEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
/**
* 表单提交控制器
* ========================
* Created with IntelliJ IDEA.
* User:恒宇少年
* Date:2017/9/16
* Time:22:26
* 码云:http://git.oschina.net/jnyqy
* ========================
*/
@RestController
public class IndexController
{
/**
*
* @param student
* @return
* @throws Exception
*/
@RequestMapping(value = "/submit")
@ContentSecurity
public String security
(@ContentSecurityAttribute("student") @Valid StudentEntity student)
throws Exception
{
System.out.println(JSON.toJSON(student));
return "SUCCESS";
}
}
在IndexController
控制器内添加一个名为submit
的方法,该方法上我们配置了@ContentSecurity
安全拦截注解,也就是会走ContentSecurityInterceptor
解密逻辑,在参数StudentEntity
上配置了两个注解,分别是:@ContentSecurityAttribute
、@Valid
,其中@ContentSecurityAttribute
则是指定了与参数student
同样的值,也就意味着参数装载时会直接将对应属性的值设置到student
内。
编写测试
我们在项目创建时添加的Chapter37ApplicationTests
测试类内写一个简单的测试用例,代码如下所示:
package com.yuqiyu.chapter37;
import com.alibaba.fastjson.JSON;
import com.yuqiyu.chapter37.constants.ContentSecurityConstants;
import com.yuqiyu.chapter37.utils.DES3Util;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import java.util.HashMap;
@RunWith(SpringRunner.class)
@SpringBootTest
public class Chapter37ApplicationTests {
@Autowired
private WebApplicationContext wac;
MockMvc mockMvc;
@Before
public void _init()
{
mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
}
/**
* 测试提交安全加密数据
* @throws Exception
*/
@Test
public void testSubmit() throws Exception
{
//参数列表
HashMap params = new HashMap();
params.put("name","hengyu");
params.put("age",20);
//json转换字符串后进行加密
String des = DES3Util.encrypt(JSON.toJSONString(params), DES3Util.DESKEY,"UTF-8");
MvcResult result = mockMvc.perform(
MockMvcRequestBuilders.post("/submit")
.param(ContentSecurityConstants.DES_PARAMETER_NAME,des)
)
.andDo(MockMvcResultHandlers.print())
// .andDo(MockMvcResultHandlers.log())
.andReturn();
result.getResponse().setCharacterEncoding("UTF-8");
System.out.println(result.getResponse().getContentAsString());
Assert.assertEquals("请求失败",result.getResponse().getStatus(),200);
Assert.assertEquals("提交失败",result.getResponse().getContentAsString(),"SUCCESS");
}
}
我们将参数使用DES
加密进行处理,传递加密后的参数名字与拦截器解密方法实现了一致,这样在解密时才会得到相应的值,上面代码中我们参数传递都是正常的,我们运行下测试方法看下控制台输出,如下所示:
....省略其他输出
2017-10-16 22:05:04.883 INFO 9736 --- [ main] c.y.c.i.ContentSecurityInterceptor : 请求加密参数内容:A8PZVavK1EhP0khHShkab/MvCuj+JJle0Ou+GdiPdYo=
2017-10-16 22:05:04.918 INFO 9736 --- [ main] c.y.c.i.ContentSecurityInterceptor : 解密请求后获得参数列表 >>> {"name":"hengyu","age":20}
2017-10-16 22:05:04.935 INFO 9736 --- [ main] .r.ContentSecurityMethodArgumentResolver : 映射安全字段:name,字段类型:String,字段内容:hengyu
2017-10-16 22:05:04.935 INFO 9736 --- [ main] .r.ContentSecurityMethodArgumentResolver : 映射安全字段:age,字段类型:int,字段内容:20
{"name":"hengyu","age":20}
SUCCESS
....省略其他输出
可以看到已经成功了完成了安全参数的装载,并且将参数映射相应的日志进行了打印,我们既然已经配置了@Valid
数据有效校验,下面我们测试是否生效!
我们将参数age修改为16
,我们配置的验证注解的内容为@Min(18)
,如果设置成16则请求返回的statusCode
应该是400
,下面我们再来运行下测试方法,查看控制台输出:
....省略部分输出
Resolved Exception:
Type = org.springframework.validation.BindException
......
java.lang.AssertionError: 请求失败
Expected :400
Actual :200
.....省略部分输出
确实如我们想的一样,请求所抛出的异常也正是BindException
,参数绑定异常!
总结
本章内容代码比较多,主要目的就只有一个,就是统一完成请求安全参数解密,让我们更专注与业务逻辑,省下单独处理加密参数的时间以至于提高我们的开发效率!
本章代码已经上传到码云:
SpringBoot配套源码地址:https://gitee.com/hengboy/spring-boot-chapter
SpringCloud配套源码地址:https://gitee.com/hengboy/spring-cloud-chapter
SpringBoot相关系列文章请访问:目录:SpringBoot学习目录
QueryDSL相关系列文章请访问:QueryDSL通用查询框架学习目录
SpringDataJPA相关系列文章请访问:目录:SpringDataJPA学习目录
感谢阅读!
欢迎加入QQ技术交流群,共同进步。
网友评论