springboot aop配合反射统一签名验证实践
作者:ysk_xh_521
这篇文章主要介绍了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; } }
最终实现的效果图
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。