转载请注明出处

作者:晓渡
文章地址:https://greatestrabit.github.io/2016/04/12/antireplayattack/

背景项目

前段时间因项目需要接入短信平台,发现短信平台提供的api虽然简单易用,在安全性上面的考虑基本都比较欠缺.更有甚者,直接将未加密数据暴露在网络中,虽然对密码部分进行了hash,但是有心人依然可以在不知道密码的情况下直接修改短信内容和发送对象,将短信套餐为己所用.
正常请求:http://www.duanxinpingtai.com/send.jsp?username=username&password=md5(password)&sim=13XXXXXXXXX&content=短信内容
修改后的请求:http://www.duanxinpingtai.com/send.jsp?username=username&password=md5(password)&sim=修改后的号码(13XXXXXXXXX)&content=修改后的短信内容
在考查了部分短信平台的方案之后发现,现有的安全方案主要有以下几种:

1. IP白名单

即限制请求的IP地址,对不在白名单中的请求直接丢弃.这种方式还是比较有效的,毕竟直接修改TCP源地址并不是一件容易的事情.但是这种方式对没有固定IP地址的用户来说很不友好,尤其是在IP地址逐渐枯竭的大背景下.

2. https

也有服务商提供https格式的api,不过这种方式会影响客户端性能,也同时给服务器带来较大压力.如果服务器端没有好好优化,则速度对比明显.参见SSL延迟有多大?.

3. MAC

这种方式通过对关键信息以及共享密钥联合计算hash值的方式来实现消息的验证和用户身份的验证,实现也比较简单,是服务商的首选.可惜的是,这种方式无法抵御重放***.

一般解决方案

针对第3种方案,抵御重放***的思路一般有以下3种:时间戳,递增序号和提问应答.详情请看:重放***.这3种方式在单线程调用,即保证执行顺序的时候都能较好的执行.可是,一旦客户端使用了多线程技术,也就是无法保证请求顺序的时候,合法的请求很有可能会被当成重放请求,造成大面积误判.

改进的解决方案

解决这个问题的方式在于,要在服务端设置合法序列数组.即对递增序号的方式进行改进,不使用单一的数字进行验证,而使用一组合法序号,匹配后的序号会从序列数组中被删除,同时递增新的序号,主要代码如下:

/**
 * 验证序列号是否在合法列表中,多线程情况下,该方法需要同步;该方案暂未考虑序列号重复使用的情况
 * @author xiaodu.email@gmail.com
 * @param serialNo
 * @return
 */
public synchronized boolean checkSerialNo(final int serialNo) {
	int index = validNumberArray.indexOf(serialNo);

	if (index < 0) {
		return false;
	} else {
		validNumberArray.remove(index);
		int max = validNumberArray.get(validNumberArray.size() - 1);
		validNumberArray.add(max + 1);
		int min = validNumberArray.get(0);

		if (max - min > 100) {
			validNumberArray.remove(0);
			validNumberArray.add(max + 2);
		}

		return true;
	}
}

完整的示例代码见:github