springboot aop配合反射統一簽名驗證實踐
aop配合反射統一簽名驗證
直接上代碼,作為記錄。
CheckSignAspect.java
@Aspect //定義一個切面 @Configuration @Log4j2 public class CheckSignAspect { // 定義切點Pointcut @Pointcut("execution(* com.lsj.xxl.controller.*.*CheckSign(..))") public void excudeService() { } @Around("excudeService()") public Object doAround(ProceedingJoinPoint pjp) throws Throwable { String class_name = pjp.getTarget().getClass().getName(); String method_name = pjp.getSignature().getName(); String[] paramNames = getFieldsName(class_name, method_name); Object[] method_args = pjp.getArgs(); SortedMap<String, String> map = logParam(paramNames, method_args); if (map != null) { String sign = map.get("sign").toUpperCase(); map.remove("sign"); String realSign = SignUtil.createSign("utf8", map); if (!realSign.equals(sign)) { return "簽名校驗錯誤"; } } Object result = pjp.proceed(); return result; } /** * 使用javassist來獲取方法參數名稱 * * @param class_name 類名 * @param method_name 方法名 * @return * @throws Exception */ private String[] getFieldsName(String class_name, String method_name) throws Exception { Class<?> clazz = Class.forName(class_name); String clazz_name = clazz.getName(); ClassPool pool = ClassPool.getDefault(); ClassClassPath classPath = new ClassClassPath(clazz); pool.insertClassPath(classPath); CtClass ctClass = pool.get(clazz_name); CtMethod ctMethod = ctClass.getDeclaredMethod(method_name); MethodInfo methodInfo = ctMethod.getMethodInfo(); CodeAttribute codeAttribute = methodInfo.getCodeAttribute(); LocalVariableAttribute attr = (LocalVariableAttribute) codeAttribute.getAttribute(LocalVariableAttribute.tag); if (attr == null) { return null; } String[] paramsArgsName = new String[ctMethod.getParameterTypes().length]; int pos = Modifier.isStatic(ctMethod.getModifiers()) ? 0 : 1; for (int i = 0; i < paramsArgsName.length; i++) { paramsArgsName[i] = attr.variableName(i + pos); } return paramsArgsName; } /** * 打印方法參數值 基本類型直接打印,非基本類型需要重寫toString方法 * * @param paramsArgsName 方法參數名數組 * @param paramsArgsValue 方法參數值數組 */ private SortedMap<String, String> logParam(String[] paramsArgsName, Object[] paramsArgsValue) { Map<String, String> map = new HashMap(); if (ArrayUtils.isEmpty(paramsArgsName) || ArrayUtils.isEmpty(paramsArgsValue)) { log.info("該方法沒有參數"); return null; } // StringBuffer buffer = new StringBuffer(); for (int i = 0; i < paramsArgsName.length; i++) { //參數名 String name = paramsArgsName[i]; //參數值 Object value = paramsArgsValue[i]; // if ("sign".equals(name)){ // continue; // } if (isPrimite(value.getClass())) { map.put(name, String.valueOf(value)); } else { map.put(name, value.toString()); } } return new TreeMap<>(map); } /** * 判斷是否為基本類型:包括String * * @param clazz clazz * @return true:是; false:不是 */ private boolean isPrimite(Class<?> clazz) { if (clazz.isPrimitive() || clazz == String.class) { return true; } else { return false; } } }
SignUtil.java
public class SignUtil { public static String createSign(String characterEncoding, SortedMap<String, String> parameters) { StringBuffer sb = new StringBuffer(); for (Map.Entry<String, String> entry : parameters.entrySet()) { if (!Strings.isNullOrEmpty(entry.getValue()) && !"sign".equals(entry.getKey()) && !"key".equals(entry.getKey())) { sb.append(entry.getKey() + "=" + entry.getValue() + "&"); } } String s = sb.toString(); if (s.length() > 0) { s = s.substring(0, sb.toString().length() - 1); } System.out.println("待加密字符串:" + s); String sign = MD5Util.MD5Encode(s, characterEncoding).toUpperCase(); return sign; } }
測試
@PostMapping("test1") public String bbCheckSign( @RequestParam("value") String value, @RequestParam("bb")String bb, @RequestParam("sign")String sign){ return "ok"; }
補充:
上述方式也可換為註解實現,具體修改代碼如下:
添加註解
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME ) public @interface CheckSign { }
改變切點
@Pointcut("@annotation(com.lsj.xxl.annotation.CheckSign)")
接口統一簽名校驗
實現接口請求簽名校驗,時間戳判斷,響應數據返回簽名等內容。
這個簽名校驗,和返回簽名可以用多種方法實現。
第一種aop 方式實現
自定義註解體
/** * @author xxx */ @Retention(value = RetentionPolicy.RUNTIME) public @interface SignatureValidation { }
aop實現
/** * @author xxx */ @Aspect @Component public class SignatureValidation { /** * 時間戳請求最小限制(600s) * 設置的越小,安全系數越高,但是要註意一定的容錯性 */ private static final long MAX_REQUEST = 10 * 60 * 1000L; /** * 秘鑰 */ private static final String SECRET= "test"; /** * 驗簽切點(完整的找到設置的文件地址) */ @Pointcut("execution(@com.xx.xxx.xxxxx.aop.SignatureValidation * *(..))") private void verifyUserKey() { } /** * 獲取請求數據,並校驗簽名數據 */ @Before("verifyUserKey()") public void doBasicProfiling(JoinPoint point) { ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = Objects.requireNonNull(attributes).getRequest(); String sign = "" ; String timestamp = "" ; String version = ""; SortedMap<String,String> sortedMap = new TreeMap<String,String>(); for ( Object obj :point.getArgs()) { JSONObject jsonObject =JSONUtil.parseObj(obj); if( !StrUtil.isEmptyIfStr(jsonObject.get("sign"))){ sign=jsonObject.get("sign").toString(); } if(!StrUtil.isEmptyIfStr(jsonObject.get("timestamp"))){ timestamp=jsonObject.get("timestamp").toString(); sortedMap.put("timestamp", timestamp); } if(!StrUtil.isEmptyIfStr(jsonObject.get("version"))){ version=jsonObject.get("version").toString(); sortedMap.put("version", version); } if(!StrUtil.isEmptyIfStr(jsonObject.get("data"))){ String dataStr= jsonObject.get("data").toString(); if(!JSONUtil.isJsonObj(dataStr)) { if(JSONUtil.isJsonArray(dataStr)){ sortedMap.put("data", JSONUtil.parseArray(dataStr).toString()); } sortedMap.put("data", dataStr); } else { JSONObject dataJson= JSONUtil.parseObj(dataStr); @SuppressWarnings("unchecked") Set<String> keySet = dataJson.keySet(); String key = ""; Object value = null; // 遍歷json數據,添加到SortedMap對象 for (Iterator<String> iterator = keySet.iterator(); iterator.hasNext();) { key = iterator.next(); value = dataJson.get(key); String valueStr=""; if(!StrUtil.isEmptyIfStr(value)){ valueStr=value.toString(); } sortedMap.put(key, valueStr); } } } } if (StrUtil.isEmptyIfStr(sign)) { throw new CustomException(BaseResultInfoEnum.ERROR_MISSING_SIGN_1009.getMsg(),BaseResultInfoEnum.ERROR_MISSING_SIGN_1009.getCode()); } //新的簽名 String newSign = createSign(sortedMap,SECRET); if (!newSign.equals(sign.toUpperCase())) { throw new CustomException(BaseResultInfoEnum.ERROR_SIGN_1010.getMsg(),BaseResultInfoEnum.ERROR_SIGN_1010.getCode()); } } /** * * @param point * @param responseObject 返回參數 */ @AfterReturning(pointcut="verifyUserKey()",returning="responseObject") public void afterReturning(JoinPoint point,Object responseObject) { HttpServletResponse response=((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse(); if(responseObject instanceof ResponseModel){ ResponseModel responseModel= (ResponseModel) responseObject; responseModel.setTimestamp(System.currentTimeMillis()); responseModel.setVersion(0); String sign= Md5Utils.createSign(Md5Utils.createParameters(responseModel),SECRET); responseModel.setSign(sign); } } }
md5簽名
/** * @author xxx */ public class Md5Utils { /** * 生成簽名 * @param parameters * @param key 商戶ID * @return */ public static String createSign(SortedMap<String,String> parameters, String key){ StringBuffer sb = new StringBuffer(); Set es = parameters.entrySet(); Iterator it = es.iterator(); while(it.hasNext()) { Map.Entry entry = (Map.Entry)it.next(); String k = (String)entry.getKey(); Object v = entry.getValue(); if(null != v && !"".equals(v) && !"sign".equals(k) && !"key".equals(k)) { sb.append(k + "=" + v + "&"); } } sb.append("key=" + key); String sign = SecureUtil.md5(sb.toString()).toUpperCase(); return sign; } /** * 簽名參數 * @param responseModel 響應數據簽名返回給調用者 * @return */ public static SortedMap<String,String> createParameters(ResponseModel responseModel){ SortedMap<String,String> sortedMap = new TreeMap<String,String>(); if(responseModel!=null) { sortedMap.put("timestamp", Convert.toStr(responseModel.getTimestamp()) ); sortedMap.put("version", Convert.toStr(responseModel.getVersion())); JSONObject json = JSONUtil.parseObj(responseModel, false); if(responseModel.getData()!=null) { sortedMap.put("data", json.get("data").toString()); } } return sortedMap; } }
使用,在控制中的方法上方註解即可
第二種攔截器
這裡隻做瞭時間判斷,簽名校驗可以根據需要修改即可實現。
過濾器
import javax.servlet.*; import javax.servlet.annotation.WebFilter; import javax.servlet.http.HttpServletRequest; import java.io.IOException; /** * @author xx */ @WebFilter(urlPatterns = "/*",filterName = "channelFilter") public class ChannelFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { ServletRequest requestWrapper = null; if(servletRequest instanceof HttpServletRequest) { requestWrapper = new RequestWrapper((HttpServletRequest) servletRequest); } if(requestWrapper == null) { filterChain.doFilter(servletRequest, servletResponse); } else { filterChain.doFilter(requestWrapper, servletResponse); } } @Override public void destroy() { } }
攔截器配置
import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; /** * @author xxxx */ @Configuration public class InterceptorConfig implements WebMvcConfigurer{ @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(getHandlerInterceptor()); } @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedHeaders("Content-Type", "x-requested-with", "X-Custom-Header") .allowedMethods("PUT", "POST", "GET", "DELETE", "OPTIONS") .allowedOrigins("*") .allowCredentials(true); } @Bean public FilterRegistrationBean repeatedlyReadFilter() { FilterRegistrationBean registration = new FilterRegistrationBean(); ChannelFilter repeatedlyReadFilter = new ChannelFilter(); registration.setFilter(repeatedlyReadFilter); registration.addUrlPatterns("/*"); return registration; } @Bean public HandlerInterceptor getHandlerInterceptor() { return new TimestampInterceptor(); } }
RequestWrapper 請求流重寫處理
import javax.servlet.ReadListener; import javax.servlet.ServletInputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; import java.io.*; /** * @author xxx */ public class RequestWrapper extends HttpServletRequestWrapper { private final String body; public RequestWrapper(HttpServletRequest request) { super(request); StringBuilder stringBuilder = new StringBuilder(); BufferedReader bufferedReader = null; InputStream inputStream = null; try { inputStream = request.getInputStream(); if (inputStream != null) { bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); char[] charBuffer = new char[128]; int bytesRead = -1; while ((bytesRead = bufferedReader.read(charBuffer)) > 0) { stringBuilder.append(charBuffer, 0, bytesRead); } } else { stringBuilder.append(""); } } catch (IOException ex) { } finally { if (inputStream != null) { try { inputStream.close(); } catch (IOException e) { e.printStackTrace(); } } if (bufferedReader != null) { try { bufferedReader.close(); } catch (IOException e) { e.printStackTrace(); } } } body = stringBuilder.toString(); } @Override public ServletInputStream getInputStream() throws IOException { final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body.getBytes()); ServletInputStream servletInputStream = new ServletInputStream() { @Override public boolean isFinished() { return false; } @Override public boolean isReady() { return false; } @Override public void setReadListener(ReadListener readListener) { } @Override public int read() throws IOException { return byteArrayInputStream.read(); } }; return servletInputStream; } @Override public BufferedReader getReader() throws IOException { return new BufferedReader(new InputStreamReader(this.getInputStream())); } public String getBody() { return this.body; } }
攔截器 這裡可以實現請求來的簽名處理,這裡隻處理時間瞭
import cn.hutool.core.util.StrUtil; import cn.hutool.json.JSONObject; import cn.hutool.json.JSONUtil; import xxx.xxx.xxx.common.result.BaseResultInfoEnum; import xxx.xxx.common.core.exception.CustomException; import org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController; 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; /** * @author xxx */ public class TimestampInterceptor implements HandlerInterceptor { /** * 時間戳請求最小限制(600s) * 設置的越小,安全系數越高,但是要註意一定的容錯性 */ private static final long MAX_REQUEST = 10 * 60 * 1000L; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (handler instanceof HandlerMethod) { HandlerMethod handlerMethod = (HandlerMethod) handler; if (handlerMethod.getBean() instanceof BasicErrorController) { return true; } // if ("GET".equals(request.getMethod())) { // return true; // } ValidateResponse validateResponse = new ValidateResponse(true, null); RequestWrapper myRequestWrapper = new RequestWrapper((HttpServletRequest) request); validateResponse= checkTimestamp(myRequestWrapper.getBody()); if (!validateResponse.isValidate()) { throw validateResponse.getException(); } } return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) { } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { } private ValidateResponse checkTimestamp(String requestBody) { try { JSONObject jsonObject = JSONUtil.parseObj(requestBody); String timestamp = "" ; if(!StrUtil.isEmptyIfStr(jsonObject.get("timestamp"))) { timestamp=jsonObject.get("timestamp").toString(); } if (StrUtil.isEmptyIfStr(timestamp)) { return new ValidateResponse(false, new CustomException(BaseResultInfoEnum.ERROR_MISSING_TIMESTAMP_1007.getMsg(), BaseResultInfoEnum.ERROR_MISSING_TIMESTAMP_1007.getCode())); } long now = System.currentTimeMillis(); long time = Long.parseLong(timestamp); if (now - time > MAX_REQUEST) { return new ValidateResponse(false, new CustomException(BaseResultInfoEnum.ERROR_TIMESTAMP_TIMEOUT_1008.getMsg(), BaseResultInfoEnum.ERROR_TIMESTAMP_TIMEOUT_1008.getCode())); } } catch (Exception e) { e.printStackTrace(); } return new ValidateResponse(true, null); } /** * 校驗返回對象 */ private static class ValidateResponse { private boolean validate; private CustomException exception; public ValidateResponse(boolean validate, CustomException exception) { this.validate = validate; this.exception = exception; } public boolean isValidate() { return validate; } public Exception getException() { return exception; } } }
返回給前端(或其他平臺的)處理類
import comzzzz.xx.common.pojo.PageResponseModel; import com.zzz.xxxx.common.pojo.ResponseModel; import org.springframework.core.MethodParameter; import org.springframework.http.MediaType; import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpResponse; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; /** * @author sunrh */ @ControllerAdvice public class ResponseBodyTimestamp implements ResponseBodyAdvice { @Override public boolean supports(MethodParameter methodParameter, Class aClass) { return true; } @Override public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) { //就是這裡處理返回簽名數據 if(o instanceof ResponseModel){ ResponseModel responseModel= (ResponseModel) o; responseModel.setTimestamp(System.currentTimeMillis()); return responseModel; } if(o instanceof PageResponseModel){ PageResponseModel responseModel= (PageResponseModel) o; responseModel.setTimestamp(System.currentTimeMillis()); return responseModel; } return o; } }
最終實現的效果圖
以上為個人經驗,希望能給大傢一個參考,也希望大傢多多支持WalkonNet。
推薦閱讀:
- SpringBoot 請求參數忽略大小寫的實例
- Java實現接口限流方案
- springboot清除字符串前後空格與防xss攻擊方法
- 基於Springboot+Netty實現rpc的方法 附demo
- SpringBoot全局Controller返回值格式統一