昨天收到一个安全漏洞,说是有个投票接口存在刷票的情况,需要紧急看下。
我最初偷懒在本地搞个单元测试跑了下,发现没有出现并发写的情况,只有一个线程通过,其他的都锁住了。
class voteActionRunnable implements Runnable {
@Override
public void run() {
...
// dataResult 是执行投票的方法的返回值
System.out.println(JSON.toJSONString(dataResult));
}
}
@Test
public void voteAction(){
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(new voteActionRunnable());
thread.run();
}
}
既然安全提了肯定不是空穴来风,还是完全按照真实的请求场景来,从客户端并发发起 http 请求开始。因为需要保存比较多的上下文参数,还要很直观的查看过滤结果,我就试着用 jmeter 来弄下。确实挺香。
下载安装
https://jmeter.apache.org/download_jmeter.cgi
下载一个编译好了的压缩版,不用安装了,解压即可使用
启动
怎么启动呢,看这里 https://jmeter.apache.org/usermanual/get-started.html#running 也就是说这个二进制包是windows 和 unix 类系统通用的,不区分环境。
解压完后,进入 bin 目录,因为我是 mac 环境就点击 jmeter
bin 文件,如果是 windows 点击jmeter.bat
,然后就会弹出一个 GUI 界面了
jmeter国际化做的不错,设置下语言,使用更方便
然后点击 文件
-> 新建
一个测试计划,然后就是全中文了
配置测试计划
添加线程组
设置 20 个线程来模拟用户并发请求或者攻击
线程组下添加 HTTP 请求
测试计划增加 HTTP 信息头管理器
直接从控制台复制粘贴
测试计划增加察看结果树
执行测试计划
然后可以在这里查看测试的结果,并且去做些匹配检查,比较方便。这只是这种场景的测试,功能非常强大,可以玩玩。
回归业务
在使用 jmeter 之后发现确实出现了刷票的情况,比如业务上只允许一个人一天只能投n票,20个线程并发,就发现多了很多票。最后发现(实际代码写的比较长,简化之后)类似于这样的 bug
try{
// 按照投票行为+人+投票对象加锁
boolean lock = tairService.lock(lockCacheKey, expireTime);
if(!lock){
log.error("lock not release,lockKey:{}",lockCacheKey);
return xxx;
}
// 获取到锁之后的业务逻辑
...
} catch (Exception e) {
...
} finally {
boolean unLock = tairService.unLock(lockCacheKey, expireTime);
if(!unLock){
log.error("lock release failed,lockKey:{}",lockCacheKey);
}
}
以四个并发请求为场景,可能出现如下的情况:
a 请求先拿到分布式锁,然后 b 请求拿不到锁,但是因为有 finally 操作,return 的同时会把 a 请求的锁释放了;
然后 c 请求进来就又拿到了同样的锁,继续投票;
a 请求如果先执行完,又把这个锁释放了,c 还没执行完,d 请求进来又能拿到锁,继续投票。
为什么在单元测试的时候没有发现呢?
可能是单元测试线程的并发性更高,而 jmeter 网络发起存在时差,就正好触发了。
解决方案
- 加锁那块和投票处理逻辑分开,不要共用 finally。
- 锁里面可以加随机值,防止释放锁的时候出现“越权”。这需要考分布式锁的命令的原子性。关于分布式的文章很大大家可以自行搜索,推荐框架 redisson 框架的解决方案。