会话重放攻击与完整性校验解决方案

简介:

攻击者发送一个目的主机已经接收过的包,特别是在认证的过程中,用于认证用户身份所接收的包,来达到欺骗系统的目的,主要用于身份认证过程,破坏认证的安全性。也可利用系统中POST请求数据包未针对单个请求设置有效的验证参数,导致会话请求可以重放,无限制的向数据库中插入海量数据,或无限制的上传文件到系统中,造成资源浪费。

解决方案:

  1. 后端生成当前系统时间
//用于获取当前系统时间,以毫秒为单位
@RequestMapping(value="queryCurrentTime",method=RequestMethod.GET)
public Map<String,Object> queryCurrentTime(){

	Map<String,Object> map  = new HashMap<>();
	map.put("code","SUCCESS");
	map.put("message","成功!");
	map.put("data",System.currentTimeMillis());
}
  1. 在vue中,request.js中统一进行处理,
import request from '@/utiils/request.js'
import CryptoJS from 'crypto-js'

//随机数
let guid = function(){
	return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c){
	var r=Math.random()*16 | 0,
		v = c == 'x' ? r:(r & 0x3 | 0x8);
	return v.toString(16);
	})
}


// 请求拦截器
service.interceptors.request.use(
	async (config) => {
		// 在发送请求之前做一些事情
		
		//此处只对post做处理, 如若对get请求做处理,将此post校验if去掉即可
		if(config.method =="post"){
			// 防止死循环 
			if (url.indexOf('queryCurrentTime') !== -1) { 
				return config
			}

			const req = {
				url: 'http://localhost:8888/queryCurrentTime',
				method: 'get',
			}

			//async await 同步
			await request(req).then(response => {

				// 服务器获取时间戳
				let timestamp = data.data
				const uuid = require('uuid')
				// 在localStorage中获取盐值
				// let saltValue = window.localStorage.getItem('uId')
				let saltValue = 'CURRENTIME'
				let params = ''
				if (config.method === 'post' && config.data) {
					params = decodeURIComponent(config.data)
				} else {
					if (config.params) {
						let paramsKeys = []
						paramsKeys = Object.keys(config.params)
						// 根据key进行排序
						paramsKeys = paramsKeys.sort()
						let paramsValues = []
						paramsKeys.forEach(value => {
							if(typeof config.params[value] !== 'object'){
								paramsValues.push(config.params[value])
							}
							
						})
						params = paramsValues.join('-')
					}
				}
				// console.log(params)
				// 生成随机数uuid
				let uId = uuid.v4()
				// MD5签名
				let sign = CryptoJS.md5(timestamp + uId + params + saltValue)
				config.headers.timestamp = timestamp
				config.headers.sign = sign
				config.headers.replayToken = uId
			})
		}
		return config
	},
	error => {
		// 做一些请求错误
		
		console.log(error)
		return Promise.reject(error)
	}
)
  1. 配置过滤器的启动顺序

@Configuration
public class FilterConfig{
	@Autowired
	private CommitTokenService commitTokenService ;
	
	//此处解决RedisTemplate对象无法注入的问题 !! 将过滤器定义为bean对象,交由spring管理
	//过滤器属于servlet范围,无法直接注入对象
	//springboot关于拦截器或者过滤器无法注入bean的问题 ? 参考	https://segmentfault.com/a/1190000012107467
	@Bean
	public FormFilter formFilter(){
		return new FormFilter();
	}
	
	@Bean
	public FilterRegistrationBean filterRegistrationBean (){
		FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean ();
		filterRegistrationBean.setOrder(3);
		filterRegistrationBean.setFilter(formFilter());
		filterRegistrationBean.setName("FormFilter");
		filterRegistrationBean.addUrlPatterns("/*");
		return filterRegistrationBean;
	}
}
  1. 异常处理类
public class ExceptionCommonUtils{
	pulbic static String getId(){
		return uuid.replace("-","");
	}
	//用来响应信息
	public static void responseMessage(HttpServletResponse response,Integer code,String message)throw IOException{
		Map<String,Object> map = new HashMap<>();
		map.put("code",code);
		map.put("message",message);
		response.setContentType(text/json;charset=utf-8);
		JSONObject fromObject = JSONObject.formObject(map);
		PrintWriter pw = response.getWriter();
		pw.write(fromObject.toString());
		pw.flush();
		pw.close();
		
	}
}
  1. 通过MD5加密,生成sign
public class IntegrityChecking{
	//盐值
	private static final String MARKER = "CURRENTTIME";
	
	//获取sign值
	public static String getSign(String timestamp,String raToken,String data){
	
		StringBuilder sb = new StringBuilder ();
		sb.append(timestamp);
		sb.append(raToken);
		sb.append(data);
		sb.append(MARKER);
		return DigestUtils.md5DigestAsHex(sb.toString().getBytes());
		
}
  1. ①获取前台传入的时间,token以及前台经过MD5加密的sign值,
    ②时间+token+盐值 进行MD5加密和sign值对比,不一致说明被数据篡改,触发完整性校验
    ③计算时间差, 小于等于60秒内的sign放入到redis中存储有效时间为1小时, 每次请求都去判断key是否存在,如存在,则是重放攻击
    ④如请求时间超过60秒,则判定为请求超时,也是重放攻击
public class FormFilter implements Filter{

	@Autowired
	private RedisTemplate redisTemplate;
	private static final long EXPIRTIME = 3600L; //1小时
	private static final String PREFIX = "WQ";
	
	public FormFilter (){}
	
	@Override
	public void doFilter(ServletRequest arg0,ServletResponse arg1,FilterChain arg2)throws IOException,ServletException{
	
		HttpServletRequest req = (HttpServletRequest)arg0;
		HttpServletResponse  response = (HttpServletResponse)arg1;
		
		//如果是获取时间戳的请求,直接放行,或者是get请求进行防重放,完整性校验处理
		String url = req.getRequestURI();
		if(url.indexOf("queryCurrentTime") != -1 || "GET".equals(req.getMethod())){
			arg2.doFilter(req,response);
		}else{
			//随机数
			String timestamp = req.getHeader("timestamp");
			//token
			String raToken = req.getHeader("replayToken");
			//前台MD5生成的sign密文字符串
			String sign = req.getHeader("sign");

		try{	
			//防止timestamp raToken被篡改
			if(!"".equals(timestamp) && !"".equals(raToken) && !"".equals(sign) && null != timestamp && null != raToken && null != sign){
				
				String strString = "";
				StringBuilder str = new StringBuilder();
				Enumeration<String> param = req.getParameterNames();
				
				if("POST".equals(req.getMethod())){
					while(param.hasMoreElements()){
						String key = String.valueOf(param.nextElement());
						String value = req.getParameter(key);
						str.append(key).append("=").append(value).append("&");
					}
				}else if("GET".equals(req.getMethod())){
					while(param.hasMoreElements()){
						String key = String.valueOf(param.nextElement());
						String value = req.getParameter(key);
						str.append(value).append("-");
					}
				}
				//去掉最后一个字符
				if(!"".equals(str.toString()) && str.length() > 0){
					strString = str.toString().substring(0,str.length() -1);
				}
				
				// 对比sign值
				String parSign = IntegrityChecking.getSign(timestamp,raToken,strString);
				if(!sign.equals(parSign)){
					throw new RunTimeException("请勿中途修改数据,触发完整性校验!");
				}
				
				//计算时间差
				long parseInt = Long.parseLong(timestamp);
				long currentTimeMills = System.currentTimeMills();
				long reqTime = (currentTimeMills - parseInt );
				
				//60秒内的sign放入redis中
				if(reqTime <= 60000){
					//检查key是否存在
					if(!redisTemplate.hasKey(PREFIX + sign)){
						
						//设置变量值的过期时间
						redisTemplate.opsForValue().set(PREFIX  + sign,0,EXPIRTIME,TimeUnit.SECONDS);
					}else{
					
						throw new RunTimeException("请勿重复提交表单!");
					}
				}else{
					//大于60秒,判为请求超时
					throw new RunTimeException("请求超时,请勿重复提交表单!");
				}
				arg2.doFilter(req,response);
			} else {
				throw new RunTimeException("TIMESTAMP.TOKEN.SIGN为空,请勿重复提交表单或触发完整性校验!");
			}
			
		}catch(RunTimeException e){
			ExceptionCommonUtils.responseMessage(response,e.getCode(),e.getMessage());
		}catch(Exception e){
			ExceptionCommonUtils.responseMessage(response,"50000","formFilter异常!");
		}
	}
}
  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值