一、自定义一个注解,此注解可以使用在方法上或类上

  • 使用在方法上,表示此方法需要数据校验
  • 使用在类上,表示此类下的所有方法需要数据校验
  • 此注解对无参数方法不起作用
import org.springframework.stereotype.Component;

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface DataValidate {

}

二、自定义拦截,拦截前端所有请求

  1. 检查此接口调用的方法或方法所在的类是否使用了DataValidate注解,若没有使用,表示此接口不需要校验数据;
  2. 若使用了注解,再检查此方法有没有入参,若没有入参,不需要校验数据,否在需要校验;
  3. 把前端传来的所有参数 (除了签名参数)按照参数升序生成一个json字符串(使用TreeMap方式自动排序);
  4. 把生成的json字符串通过MD5加密的结果和前端传的签名值对比,若不相等,表示此数据被篡改过,否在没有被篡改过;
  5. 数据是否被篡改校验完毕,若前端传了用户唯一标示(token),表示需要校验数据是否重复提交;
  6. 若签名和上次提交的数据的签名相等,表示是重复提交数据,若不相等,把签名保存下来,表示数据不是重复提交。
import java.security.MessageDigest;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;

import javax.annotation.PreDestroy;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.MethodParameter;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import com.alibaba.fastjson.JSON;

/**
 * 防数据被篡改和重复提交
 */

public class DataValidateInterceptor extends HandlerInterceptorAdapter implements Runnable {

	public static Map<String, TokenValue> userToken = new ConcurrentHashMap<>();
	
	// 过期时间
	private static long EXPIRED_TIME = 3600000;
	
	private static String TOKEN_NAME = "token";
	
	private static String SIGN_NAME = "sign";
	
	private volatile boolean shutDown;
	
	public DataValidateInterceptor(@Value("${data_interceptor.expired_time}") String expiredTime,
			@Value("${data_interceptor.token_name}") String tokenName,
			@Value("${data_interceptor.sign_name}") String signName) {
		if (null != expiredTime && !"".equals(expiredTime)) {
			EXPIRED_TIME = Long.parseLong(expiredTime);
		}
		if (null != tokenName && !"".equals(tokenName)) {
			TOKEN_NAME = tokenName;
		}
		if (null != signName && !"".equals(signName)) {
			SIGN_NAME = signName;
		}
	}
	
	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {
		if (validate(request, response, handler)) {
			/**
			 * 实现返回提示数据
			 */
			response.setContentType("application/json");
			response.setCharacterEncoding("UTF-8");
			response.getWriter().write("参数验证失败!");
			return false;
		}
		return true;
	}
	
	private boolean validate(HttpServletRequest request, HttpServletResponse response, Object handler) {
		
		if (handler instanceof HandlerMethod) {
			Class<?> clazz = ((HandlerMethod) handler).getBeanType();
			DataValidate dataValidate = clazz.getAnnotation(DataValidate.class);
			if (null == dataValidate) {
				dataValidate = ((HandlerMethod) handler).getMethodAnnotation(DataValidate.class);
			}
			
			if (dataValidate != null) {
				MethodParameter[] methodParameters = ((HandlerMethod) handler).getMethodParameters();
				if (null == methodParameters || methodParameters.length <=0) {
					// 方法没有入参不需要校验
					return false;
				}
				
				// 需要校验
				String sign = request.getParameter(SIGN_NAME);
				Map<String, String[]> params = request.getParameterMap();
				Set<String> paramNames = params.keySet();
				Map<String, String> paramsMap = new TreeMap<>();
				for (String paramName : paramNames) {
					if (paramName.equals(SIGN_NAME)) {
						continue;
					}
					paramsMap.put(paramName, request.getParameter(paramName));
				}
				String paramString = JSON.toJSONString(paramsMap).replaceAll(" ", "");
				String MD5Sign = MD5.getMD5(paramString);
				if (!sign.equals(MD5Sign)) {
					// 数据被篡改
					return true;
				}
				
				String token = request.getParameter(TOKEN_NAME);
				if (token != null) {
					if (userToken.containsKey(token)) {
						TokenValue tokenValue = userToken.get(token);
						if (tokenValue.getValue().equals(sign)) {
							// 数据已经提交过
							return true;
						} else {
							tokenValue.setValue(sign);
						}
					} else {
						userToken.put(token, new TokenValue(sign));
					}
				}
				
			}
		}
		return false;
	}

	@Override
	public void run() {
		try {
			while (!shutDown) {
				synchronized (this) {
					wait(EXPIRED_TIME);
					Set<String> keys = userToken.keySet();
					for (String key : keys) {
						if ((userToken.get(key).getExpiredTime() + EXPIRED_TIME) <= System.currentTimeMillis()) {
							userToken.remove(key);
						}
					}
				}
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	@PreDestroy
	public void custDestroy() {
		shutDown = true;
		synchronized (this) {
			notifyAll();
		}
	}
	
	private static class MD5 {
		
		/**
	     * 向getMD5方法传入一个你需要转换的原始字符串,将返回字符串的MD5码
	     * 
	     * @param code 原始字符串
	     * @return 返回字符串的MD5码
	     */
	    private static String getMD5(String code) {
	        try {
	            MessageDigest messageDigest = MessageDigest.getInstance("MD5");

	            byte[] bytes = code.getBytes();

	            byte[] results = messageDigest.digest(bytes);

	            StringBuilder stringBuilder = new StringBuilder();

	            for (byte result : results) {
	                // 将byte数组转化为16进制字符存入stringbuilder中
	                stringBuilder.append(String.format("%02x", result));
	            }

	            return stringBuilder.toString();
	        } catch (Exception e) {
	            e.printStackTrace();
	            return "";
	        }
	    }
	}

}

public class TokenValue {

	private String value;
	private long expiredTime;
	
	public TokenValue(String value) {
		this.value = value;
		this.expiredTime = System.currentTimeMillis();
	}
	
	public String getValue() {
		return value;
	}
	public void setValue(String value) {
		this.value = value;
		this.expiredTime = System.currentTimeMillis();
	}
	public long getExpiredTime() {
		return expiredTime;
	}
}

三、使用

  • 后端使用:
  1. 在需要数据校验的方法或类上使用DataValidate注解(若在类上使用,表示此类下的所有方法需要验证)
  2. 配置 DataValidateInterceptor 拦截器
  3. 配置前端签名参数,默认是sign
  4. 配置用户唯一标示参数,默认是token(防止数据重复提交需要)
  5. 配置用户唯一标示过期时间,默认是1h,单位是ms(防止数据重复提交需要)
  • 前端使用:
  1. 获取用户唯一标示(防止数据重复提交需要)
  2. 把需要提交的数据根据参数(包括用户唯一标示)升序排序然后生成一个json字符串(不能有空格),最后把json字符串进行MD5加密生成32位小写加密结果签名
1.  eg:需要提交的数据为{messageType: "userQueryWait", companyCode: "test_app", token:"123213213"},排序后生成json字符串 {"companyCode":"test_app","messageType":"userQueryWait","token":"123213213"}, md5生成签名
  1. 把签名和需要提交的数据一起传到后台
6.  eg:{messageType: "userQueryWait", companyCode: "test_app", token:"123213213", sign:"719bdb1fb769efb68e40440d1628ed5b"}