在上一章第三十六章:基于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í)行WebDataBinder
的validate
方法就可以完成數(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)我們添加了兩個屬性公荧,name
、age
同规,其中都做了驗證注解配置循狰,那我們下面就針對該實體添加一個控制器方法來進行測試安全參數(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