第三十七章:基于SpringBoot架構(gòu)以及參數(shù)裝載完成接口安全認證

在上一章第三十六章:基于SpringBoot架構(gòu)重寫SpringMVC請求參數(shù)裝載中我們說到了怎么去重寫SpringMVC參數(shù)裝載媳纬,從而來完成我們的需求填渠。本章內(nèi)容會在上一章的基礎(chǔ)上進行修改!

企業(yè)中接口編寫是再頻繁不過的事情了谎倔,現(xiàn)在接口已經(jīng)不僅僅用于移動端來做數(shù)據(jù)服務(wù)了柳击,一些管理平臺也同樣采用了這種方式來完成前后完全分離的模式。不管是接口也好片习、分離模式也好都會涉及到數(shù)據(jù)安全的問題捌肴,那我們怎么可以很好的避免我們的數(shù)據(jù)參數(shù)暴露呢?

免費教程專題

恒宇少年在博客整理三套免費學習教程專題毯侦,由于文章偏多特意添加了閱讀指南哭靖,新文章以及之前的文章都會在專題內(nèi)陸續(xù)填充具垫,希望可以幫助大家解惑更多知識點侈离。

本章目標

基于SpringBoot平臺實現(xiàn)參數(shù)安全傳輸。

SpringBoot 企業(yè)級核心技術(shù)學習專題


專題 專題名稱 專題描述
001 Spring Boot 核心技術(shù) 講解SpringBoot一些企業(yè)級層面的核心組件
002 Spring Boot 核心技術(shù)章節(jié)源碼 Spring Boot 核心技術(shù)簡書每一篇文章碼云對應(yīng)源碼
003 Spring Cloud 核心技術(shù) 對Spring Cloud核心技術(shù)全面講解
004 Spring Cloud 核心技術(shù)章節(jié)源碼 Spring Cloud 核心技術(shù)簡書每一篇文章對應(yīng)源碼
005 QueryDSL 核心技術(shù) 全面講解QueryDSL核心技術(shù)以及基于SpringBoot整合SpringDataJPA
006 SpringDataJPA 核心技術(shù) 全面講解SpringDataJPA核心技術(shù)
007 SpringBoot核心技術(shù)學習目錄 SpringBoot系統(tǒng)的學習目錄筝蚕,敬請關(guān)注點贊X阅搿!!

構(gòu)建項目

本章所需要的依賴比較少起宽,我們添加相應(yīng)的Web依賴即可洲胖,下面是pom.xml配置文件部分依賴內(nèi)容:

<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>

本章的實現(xiàn)思路是采用SpringMvc攔截器來完成指定注解的攔截,并且根據(jù)攔截做出安全屬性的處理坯沪,再結(jié)合自定義的參數(shù)裝載完成對應(yīng)參數(shù)的賦值绿映。

ContentSecurityMethodArgumentResolver

我們先來創(chuàng)建一個參數(shù)裝載實現(xiàn)類,該參數(shù)狀態(tài)實現(xiàn)類繼承至BaseMethodArgumentResolver腐晾,而BaseMethodArgumentResolver則是實現(xiàn)了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.reibang.com/u/092df3f77bca
 * ================================
 */
public abstract class BaseMethodArgumentResolver
    implements HandlerMethodArgumentResolver
{

    /**
     * 獲取指定前綴的參數(shù):包括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());
                    //如果下一個字符不是 數(shù)字 . _  則不可能是查詢 只是前綴類似
                    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());
                    //如果下一個字符不是 數(shù)字 . _  則不可能是查詢 只是前綴類似
                    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架構(gòu)重寫SpringMVC請求參數(shù)裝載有什么區(qū)別,實現(xiàn)思路幾乎是一樣的藻糖,只是做部分內(nèi)容做出了修改淹冰,代碼如下所示:

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;

/**
 * 自定義方法參數(shù)映射
 * 實現(xiàn)了HandlerMethodArgumentResolver接口內(nèi)的方法supportsParameter & resolveArgument
 * 通過supportsParameter方法判斷僅存在@ContentSecurityAttribute注解的參數(shù)才會執(zhí)行resolveArgument方法實現(xiàn)
 * ===============================
 * Created with IntelliJ IDEA.
 * User:于起宇
 * Date:2017/10/11
 * Time:23:05
 * 簡書:http://www.reibang.com/u/092df3f77bca
 * ================================
 */
public class ContentSecurityMethodArgumentResolver
    extends BaseMethodArgumentResolver
{
    private Logger logger = LoggerFactory.getLogger(ContentSecurityMethodArgumentResolver.class);
    /**
     * 判斷參數(shù)是否配置了@ContentSecurityAttribute注解
     * 如果返回true則執(zhí)行resolveArgument方法
     * @param parameter
     * @return
     */
    @Override
    public boolean supportsParameter(MethodParameter parameter)
    {
        return parameter.hasParameterAnnotation(ContentSecurityAttribute.class);
    }

    /**
     * 執(zhí)行參數(shù)映射
     * @param parameter 參數(shù)對象
     * @param mavContainer 參數(shù)集合
     * @param request 本地請求對象
     * @param binderFactory 綁定參數(shù)工廠對象
     * @return
     * @throws Exception
     */
    @Override
    public Object resolveArgument(
            MethodParameter parameter,
            ModelAndViewContainer mavContainer,
            NativeWebRequest request,
            WebDataBinderFactory binderFactory) throws Exception
    {
        //獲取@ContentSecurityAttribute配置的value值,作為參數(shù)名稱
        String name = parameter.getParameterAnnotation(ContentSecurityAttribute.class).value();
        /**
         * 獲取值
         * 如果請求集合內(nèi)存在則直接獲取
         * 如果不存在則調(diào)用createAttribute方法創(chuàng)建
         */
        Object target = (mavContainer.containsAttribute(name)) ?
                mavContainer.getModel().get(name) : createAttribute(name, parameter, binderFactory, request);
        /**
         * 創(chuàng)建參數(shù)綁定
         */
        WebDataBinder binder = binderFactory.createBinder(request, target, name);
        //獲取返回值實例
        target = binder.getTarget();
        //如果存在返回值
        if (target != null) {
            /**
             * 設(shè)置返回值對象內(nèi)的所有field得值巨柒,從request.getAttribute方法內(nèi)獲取
             */
            bindRequestAttributes(binder, request);
            /**
             * 調(diào)用@Valid驗證參數(shù)有效性
             */
            validateIfApplicable(binder, parameter);
            /**
             * 存在參數(shù)綁定異常
             * 拋出異常
             */
            if (binder.getBindingResult().hasErrors()) {
                if (isBindExceptionRequired(binder, parameter)) {
                    throw new BindException(binder.getBindingResult());
                }
            }
        }
        /**
         * 轉(zhuǎn)換返回對象
         */
        target = binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType());
        //存放到model內(nèi)
        mavContainer.addAttribute(name, target);

        return target;
    }

    /**
     * 綁定請求參數(shù)
     * @param binder
     * @param nativeWebRequest
     * @throws Exception
     */
    protected void bindRequestAttributes(
            WebDataBinder binder,
            NativeWebRequest nativeWebRequest) throws Exception {

        /**
         * 獲取返回對象實例
         */
        Object obj = binder.getTarget();
        /**
         * 獲取返回值類型
         */
        Class<?> targetType = binder.getTarget().getClass();
        /**
         * 轉(zhuǎn)換本地request對象為HttpServletRequest對象
         */
        HttpServletRequest request =
                nativeWebRequest.getNativeRequest(HttpServletRequest.class);
        /**
         * 獲取所有attributes
         */
        Enumeration attributeNames = request.getAttributeNames();
        /**
         * 遍歷設(shè)置值
         */
        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);
            }
            /**
             * 如果返回對象類型內(nèi)不存在字段
             * 則從父類讀取
             */
            catch (NoSuchFieldException e)
            {
                try {
                    field = targetType.getSuperclass().getDeclaredField(fieldName);
                }catch (NoSuchFieldException e2)
                {
                    continue;
                }
                /**
                 * 如果父類還不存在樱拴,則直接跳出循環(huán)
                 */
                if(StringUtils.isEmpty(field)) {
                    continue;
                }
            }
            /**
             * 設(shè)置字段的值
             */
            field.setAccessible(true);
            String fieldClassName = field.getType().getSimpleName();
            Object attributeObj = request.getAttribute(attributeName);

            logger.info("映射安全字段:{}柠衍,字段類型:{},字段內(nèi)容:{}",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方法我們是完成了參數(shù)包含ContentSecurityAttribute注解才會做裝載處理晶乔,也就是說只要參數(shù)配置了ContentSecurityAttribute注解才會去執(zhí)行resolveArgument方法內(nèi)的業(yè)務(wù)邏輯并做出相應(yīng)的返回珍坊。注解內(nèi)容如下所示:

package com.yuqiyu.chapter37.annotation;

import java.lang.annotation.*;

/**
 * 配置該注解表示從request.attribute內(nèi)讀取對應(yīng)實體參數(shù)值
 * ========================
 * 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 {
    /**
     * 參數(shù)值
     * 對應(yīng)配置@ContentSecurityAttribute注解的參數(shù)名稱即可
     * @return
     */
    String value();
}

在上面注解代碼內(nèi)我們添加了一個屬性value,這個屬性是配置的參數(shù)的映射名稱正罢,其實目的跟@RequestParam有幾分相似垫蛆,在我們配置使用的時候保持value與參數(shù)名稱一致就可以了。
接下來我們還需要創(chuàng)建一個注解腺怯,因為我們不希望所有的請求都被做出處理袱饭!

ContentSecurity

該注解配置在控制器內(nèi)的方法上,只要配置了該注解就會被處理一些安全機制呛占,我們先來看看該注解的代碼虑乖,至于具體怎么使用以及內(nèi)部做出了什么安全機制,一會我們再來詳細講解晾虑,代碼如下:

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
{
    /**
     * 內(nèi)容加密方式
     * 默認DES
     * @return
     */
    ContentSecurityAway away() default ContentSecurityAway.DES;
}

在注解內(nèi)我們添加了away屬性方法疹味,而該屬性方法我們采用了一個枚舉的方式完成,我們先來看看枚舉的值再來說下作用帜篇,如下所示:

package com.yuqiyu.chapter37.enums;

/**
 * 內(nèi)容安全處理方式
 * 目前可配置: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內(nèi)目前我們僅聲明了一個類型DES笙隙,其實這個枚舉創(chuàng)建是為了以后的擴展洪灯,如果說以后我們的加密方式會存在多種,只需要在ContentSecurityAway添加對應(yīng)的配置竟痰,以及處理安全機制部分做出調(diào)整签钩,其他部分不需要做出任何修改。
那現(xiàn)在我們可以說萬事俱備就差處理安全機制了坏快,在文章的開頭有說到铅檩,我們需要采用攔截器來完成安全的認證,那么我們接下來看看攔截器的實現(xiàn)莽鸿。

ContentSecurityInterceptor

ContentSecurityInterceptor攔截器實現(xiàn)HandlerInterceptor接口昧旨,并且需要我們重寫內(nèi)部的三個方法,分別是preHandle祥得、postHandle兔沃、afterCompletion,我們本章其實只需要將安全認證處理編寫在preHandle方法內(nèi)啃沪,因為我們需要在請求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);

    /**
     * 請求之前處理加密內(nèi)容
     * @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();
        /**
         * 檢查是否存在內(nèi)容安全驗證注解
         */
        ContentSecurity security = method.getAnnotation(ContentSecurity.class);
        /**
         * 存在注解做出不同方式認證處理
         */
        if (security != null) {
            switch (security.away())
            {
                //DES方式內(nèi)容加密處理
                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方式內(nèi)容
     * @param request
     * @param response
     * @return
     */
    boolean checkDES(HttpServletRequest request,HttpServletResponse response) throws Exception
    {
        //獲取desString加密內(nèi)容
        String des = request.getParameter(ContentSecurityConstants.DES_PARAMETER_NAME);
        logger.info("請求加密參數(shù)內(nèi)容:{}",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參數(shù)列表并重新添加到request內(nèi)
         */
        try {
            des = DES3Util.decrypt(des, DES3Util.DESKEY,"UTF-8");

            if (!StringUtils.isEmpty(des)) {

                JSONObject params = JSON.parseObject(des);

                logger.info("解密請求后獲得參數(shù)列表  >>> {}", des);
                Iterator it = params.keySet().iterator();
                while (it.hasNext()) {
                    /**
                     * 獲取請求參數(shù)名稱
                     */
                    String parameterName = it.next().toString();
                    /**
                     * 參數(shù)名稱不為空時將值設(shè)置到request對象內(nèi)
                     * 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自定義安全注解缰雇,如果存在則是證明了該方法需要我們做安全解密入偷,客戶端傳遞參數(shù)的時候應(yīng)該是已經(jīng)按照預先定于的規(guī)則進行加密處理的。

接下來就是根據(jù)配置的加密方式進行ContentSecurityAway枚舉類型switch case選擇械哟,根據(jù)不同的配置執(zhí)行不同的解密方法疏之。

因為我們的ContentSecurityAway`注解內(nèi)僅配置了DES方式,我們就來看看checkDES``方法是怎么與客戶端傳遞參數(shù)的約定暇咆,當然這個約定這里只是一個示例锋爪,如果你的項目需要更復雜的加密形式直接進行修改就可以了。

上面代碼中最主要的一部分則是爸业,如下所示:

...省略部分代碼
des = DES3Util.decrypt(des, DES3Util.DESKEY,"UTF-8");

            if (!StringUtils.isEmpty(des)) {

                JSONObject params = JSON.parseObject(des);

                logger.info("解密請求后獲得參數(shù)列表  >>> {}", des);
                Iterator it = params.keySet().iterator();
                while (it.hasNext()) {
                    /**
                     * 獲取請求參數(shù)名稱
                     */
                    String parameterName = it.next().toString();
                    /**
                     * 參數(shù)名稱不為空時將值設(shè)置到request對象內(nèi)
                     * key=>value
                     */
                    if (!StringUtils.isEmpty(parameterName)) {
                        request.setAttribute(ContentSecurityConstants.ATTRIBUTE_PREFFIX + parameterName,params.get(parameterName));
                    }
                }
            }
....省略部分代碼

在平時其骄,客戶端發(fā)起請求時參數(shù)都是在HttpServletRequest對象的Parameter內(nèi),如果我們做出解密后是無法再次將參數(shù)存放到Parameter內(nèi)的扯旷,因為不可修改拯爽,HttpServletRequest不允許讓這么處理參數(shù),也是防止請求參數(shù)被篡改钧忽!

既然這種方式不可以毯炮,那么我就采用Attribute方式設(shè)置,將加密字符串解密完成獲取相應(yīng)參數(shù)后,將每一個參數(shù)設(shè)置的Attribute請求屬性集合內(nèi),這里你可能會有一個疑問卿嘲,我們什么時候獲取Attribute的值呢?

其實在上面代碼ContentSecurityMethodArgumentResolver類內(nèi)的方法bindRequestAttributes內(nèi)为迈,我們就已經(jīng)從Attribute獲取所有的屬性列表,然后通過反射機制設(shè)置到配置ContentSecurityAttribute安全注解屬性的參數(shù)對象內(nèi)奈揍,然而我們這種方式目前是僅僅支持實體類曲尸,而基本數(shù)據(jù)封裝類型目前沒有做處理赋续。

這樣在處理完成反射對象設(shè)置對應(yīng)字段的屬性后男翰。然后通過resolveArgument方法將參數(shù)對象實例返回就完成了參數(shù)的自定義裝載過程。

處理參數(shù)數(shù)據(jù)驗證

我們既然自定義了參數(shù)裝載纽乱,當然不能忘記處理參數(shù)的驗證機制蛾绎,這也是Spring MVC引以為傲的功能模塊之一,Spring MVC Validator其實是采用了Hibernate Validator機制完成的數(shù)據(jù)驗證鸦列,我們只需要判斷參數(shù)是否存在@Valid注解是否存在租冠,如果存在則去執(zhí)行WebDataBindervalidate方法就可以完成數(shù)據(jù)有效性驗證,相關(guān)代碼如下所示:

/**
     * 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參數(shù)裝載類內(nèi)薯嗤,到目前為止我們的參數(shù)狀態(tài)從攔截 > 驗證 > 裝載一整個過程已經(jīng)編寫完成顽爹,下面我們配置下相關(guān)的攔截器以及安全參數(shù)裝載讓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("/**");
    }
}

我們配置安全攔截器攔截所有/**根下的請求镜粤。下面配置下參數(shù)裝載捏题,在WebMvcConfigurerAdapter抽象類內(nèi)有一個方法addArgumentResolvers就可以完成自定義參數(shù)裝載配置,代碼如下所示:

    /**
     * 添加參數(shù)裝載
     * @param argumentResolvers
     */
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
        /**
         * 將自定義的參數(shù)裝載添加到spring內(nèi)托管
         */
        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;
}

在上述測試實體類內(nèi)我們添加了兩個屬性公荧,nameage同规,其中都做了驗證注解配置循狰,那我們下面就針對該實體添加一個控制器方法來進行測試安全參數(shù)裝載。

測試控制器

創(chuàng)建一個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控制器內(nèi)添加一個名為submit的方法绪钥,該方法上我們配置了@ContentSecurity安全攔截注解,也就是會走ContentSecurityInterceptor解密邏輯关炼,在參數(shù)StudentEntity上配置了兩個注解昧识,分別是:@ContentSecurityAttribute@Valid盗扒,其中@ContentSecurityAttribute則是指定了與參數(shù)student同樣的值跪楞,也就意味著參數(shù)裝載時會直接將對應(yīng)屬性的值設(shè)置到student內(nèi)。

編寫測試

我們在項目創(chuàng)建時添加的Chapter37ApplicationTests測試類內(nèi)寫一個簡單的測試用例侣灶,代碼如下所示:

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();
    }

    /**
     * 測試提交安全加密數(shù)據(jù)
     * @throws Exception
     */
    @Test
    public void testSubmit() throws Exception
    {
        //參數(shù)列表
        HashMap params = new HashMap();
        params.put("name","hengyu");
        params.put("age",20);

        //json轉(zhuǎn)換字符串后進行加密
        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");
    }
}

我們將參數(shù)使用DES加密進行處理甸祭,傳遞加密后的參數(shù)名字與攔截器解密方法實現(xiàn)了一致,這樣在解密時才會得到相應(yīng)的值褥影,上面代碼中我們參數(shù)傳遞都是正常的池户,我們運行下測試方法看下控制臺輸出,如下所示:

....省略其他輸出
2017-10-16 22:05:04.883  INFO 9736 --- [           main] c.y.c.i.ContentSecurityInterceptor       : 請求加密參數(shù)內(nèi)容:A8PZVavK1EhP0khHShkab/MvCuj+JJle0Ou+GdiPdYo=
2017-10-16 22:05:04.918  INFO 9736 --- [           main] c.y.c.i.ContentSecurityInterceptor       : 解密請求后獲得參數(shù)列表  >>> {"name":"hengyu","age":20}
2017-10-16 22:05:04.935  INFO 9736 --- [           main] .r.ContentSecurityMethodArgumentResolver : 映射安全字段:name凡怎,字段類型:String校焦,字段內(nèi)容:hengyu
2017-10-16 22:05:04.935  INFO 9736 --- [           main] .r.ContentSecurityMethodArgumentResolver : 映射安全字段:age,字段類型:int统倒,字段內(nèi)容:20
{"name":"hengyu","age":20}
SUCCESS
....省略其他輸出

可以看到已經(jīng)成功了完成了安全參數(shù)的裝載寨典,并且將參數(shù)映射相應(yīng)的日志進行了打印,我們既然已經(jīng)配置了@Valid數(shù)據(jù)有效校驗房匆,下面我們測試是否生效耸成!

我們將參數(shù)age修改為16,我們配置的驗證注解的內(nèi)容為@Min(18)浴鸿,如果設(shè)置成16則請求返回的statusCode應(yīng)該是400井氢,下面我們再來運行下測試方法,查看控制臺輸出:

....省略部分輸出
Resolved Exception:
             Type = org.springframework.validation.BindException
......
java.lang.AssertionError: 請求失敗 
Expected :400
Actual   :200
.....省略部分輸出

確實如我們想的一樣岳链,請求所拋出的異常也正是BindException花竞,參數(shù)綁定異常!

總結(jié)

本章內(nèi)容代碼比較多掸哑,主要目的就只有一個约急,就是統(tǒng)一完成請求安全參數(shù)解密寇仓,讓我們更專注與業(yè)務(wù)邏輯,省下單獨處理加密參數(shù)的時間以至于提高我們的開發(fā)效率烤宙!

本章代碼已經(jīng)上傳到碼云:
SpringBoot配套源碼地址:https://gitee.com/hengboy/spring-boot-chapter
SpringCloud配套源碼地址:https://gitee.com/hengboy/spring-cloud-chapter

作者個人 博客
使用開源框架 ApiBoot 助你成為Api接口服務(wù)架構(gòu)師

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末遍烦,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子躺枕,更是在濱河造成了極大的恐慌服猪,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,542評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件拐云,死亡現(xiàn)場離奇詭異罢猪,居然都是意外死亡,警方通過查閱死者的電腦和手機叉瘩,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,596評論 3 385
  • 文/潘曉璐 我一進店門膳帕,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人薇缅,你說我怎么就攤上這事危彩。” “怎么了泳桦?”我有些...
    開封第一講書人閱讀 158,021評論 0 348
  • 文/不壞的土叔 我叫張陵汤徽,是天一觀的道長。 經(jīng)常有香客問我灸撰,道長谒府,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,682評論 1 284
  • 正文 為了忘掉前任浮毯,我火速辦了婚禮完疫,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘债蓝。我一直安慰自己壳鹤,他們只是感情好,可當我...
    茶點故事閱讀 65,792評論 6 386
  • 文/花漫 我一把揭開白布惦蚊。 她就那樣靜靜地躺著器虾,像睡著了一般。 火紅的嫁衣襯著肌膚如雪蹦锋。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,985評論 1 291
  • 那天欧芽,我揣著相機與錄音莉掂,去河邊找鬼。 笑死千扔,一個胖子當著我的面吹牛憎妙,可吹牛的內(nèi)容都是我干的库正。 我是一名探鬼主播,決...
    沈念sama閱讀 39,107評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼厘唾,長吁一口氣:“原來是場噩夢啊……” “哼褥符!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起抚垃,我...
    開封第一講書人閱讀 37,845評論 0 268
  • 序言:老撾萬榮一對情侶失蹤喷楣,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后鹤树,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體铣焊,經(jīng)...
    沈念sama閱讀 44,299評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,612評論 2 327
  • 正文 我和宋清朗相戀三年罕伯,在試婚紗的時候發(fā)現(xiàn)自己被綠了曲伊。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,747評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡追他,死狀恐怖坟募,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情邑狸,我是刑警寧澤婿屹,帶...
    沈念sama閱讀 34,441評論 4 333
  • 正文 年R本政府宣布,位于F島的核電站推溃,受9級特大地震影響昂利,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜铁坎,卻給世界環(huán)境...
    茶點故事閱讀 40,072評論 3 317
  • 文/蒙蒙 一蜂奸、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧硬萍,春花似錦扩所、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,828評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至买羞,卻和暖如春袁勺,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背畜普。 一陣腳步聲響...
    開封第一講書人閱讀 32,069評論 1 267
  • 我被黑心中介騙來泰國打工期丰, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 46,545評論 2 362
  • 正文 我出身青樓钝荡,卻偏偏與公主長得像街立,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子埠通,可洞房花燭夜當晚...
    茶點故事閱讀 43,658評論 2 350

推薦閱讀更多精彩內(nèi)容