第三十七章:基于SpringBoot架构以及参数装载完成接口安全认证

在上一章第三十六章:基于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核心技术

构建项目

本章所需要的依赖比较少,我们添加相应的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接口,并且需要我们重写内部的三个方法,分别是preHandlepostHandleafterCompletion,我们本章其实只需要将安全认证处理编写在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注解是否存在,如果存在则去执行WebDataBindervalidate方法就可以完成数据有效性验证,相关代码如下所示:

/**
     * 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;
}

在上述测试实体类内我们添加了两个属性,nameage,其中都做了验证注解配置,那我们下面就针对该实体添加一个控制器方法来进行测试安全参数装载。

测试控制器

创建一个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学习目录 感谢阅读!

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏大内老A

依赖注入[5]: 创建一个简易版的DI框架[下篇]

为了让读者朋友们能够对.NET Core DI框架的实现原理具有一个深刻而认识,我们采用与之类似的设计构架了一个名为Cat的DI框架。在《依赖注入[4]: 创建...

593
来自专栏java技术学习之道

事物在Controller层的探索

933
来自专栏java、Spring、技术分享

Netty中Channel与Unsafe源码解读

  Channel是netty网络操作抽象类,包括网络的读,写,链路关闭,发起连接等。我们拿出NioServerSocketChannel来进行分析,NioSe...

943
来自专栏木木玲

Netty 源码解析 ——— 服务端启动流程 (下)

1656
来自专栏好好学java的技术栈

jdbc就是这么简单

JDBC(Java DataBase Connectivity,java数据库连接)是一种用于执行SQL语句的Java API,可以为多种关系数据库提供统一访问...

643
来自专栏wannshan(javaer,RPC)

dubbo 缓存的使用和实现解析

dubbo缓存主要实现,对方法调用结果的缓存。 在服务消费方和提供方都可以配置使用缓存。 以消费方为例,可以配置全局缓存策略,这样所有服务引用都启动缓存 ...

2786
来自专栏芋道源码1024

数据库分库分表中间件 Sharding-JDBC 源码分析 —— SQL 执行

本文主要基于 Sharding-JDBC 1.5.0 正式版 1. 概述 2. ExecutorEngine 2.1 ListeningExecutorServ...

3507
来自专栏IT可乐

mybatis源码解读(三)——数据源的配置

1133
来自专栏Jed的技术阶梯

Kafka 消费者旧版低级 API

Kafka 消费者总共有 3 种 API,新版 API、旧版高级 API、旧版低级 API,新版 API 是在 kafka 0.9 版本后增加的,推荐使用新版 ...

923
来自专栏13blog.site

Spring+SpringMVC+MyBatis+easyUI整合优化篇(四)单元测试实例

前言 前一篇文章《Spring+SpringMVC+MyBatis+easyUI整合优化篇(三)代码测试》讲了不为和不能两个状态,针对不为,只能自己调整心态了,...

2835

扫码关注云+社区