Cache解决POST请求重复发送的问题

问题产生原因:
1.前端连续点击按钮导致重复发送请求
2.刷新页面或者点击返回导致的请求重复发送问题
3.运行脚本或者手动AJAX重复POST请求,带来的重复提交问题

前两者,均可以通过前端disable提交按键,或者增加相关判断使得前端仅仅发送一次POST请求。但是无法从根本解决后台对于重复的POST请求的正确处理。如果攻击者有意绕开前端,直接运行脚本连续发送重复的POST请求,则后台无法对该情况进行正确的处理。
后台解决该问题主要通过,每次处理POST请求时,均对该请求进行逻辑上的check,是否重复处理。
Duplicate Check可以通过直接从数据库中拿相关数据,对本次请求进行check,但是直接从数据库获取数据有两个问题:
1.速度较慢
2.如果数据库不支持事务性如Cassandra,则对于高并发情况下的请求校验,会产生race condition bug。

以上两个问题可以通过Cache来解决,可以将相关POST请求的业务状态保存在cache中,duplicate check时可以快速获取所需状态,同时选取的cache支持显示锁或者事务性,即可解决请求的竞态问题。
Ehcache:ehcache直接在jvm虚拟机中缓存,速度快,效率高;但是缓存共享麻烦,集群分布式应用不方便。
Redis:redis是通过socket访问到缓存服务,效率比ecache低,比数据库要快很多,处理集群和分布式缓存方便,有成熟的方案。如果是单个应用或者对缓存访问要求很高的应用,用ehcache。如果是大型系统,存在缓存共享、分布式部署、缓存内容很大的,建议用redis。

一开始没有考虑公司项目,采用Ehcache给出了解决方案,通过ehcache保存业务状态,利用ehcache支持的显示锁,支持在Key上加锁,解决相同请求的竞态问题,同时避免了大量不同请求的阻塞而影响性能。

public class EhcacheUtil {
    static CacheManager cacheManager = CacheManager.create();
    static Cache cache = cacheManager.getCache("myCache");
    public static boolean duplicateCheck(String id) {
        cache.acquireWriteLockOnKey(id);
        Element element = cache.get(id);
        //业务逻辑判断是否重复发送
        cache.releaseWriteLockOnKey(announcementId);
        return true;
    }
}

cache存在一定时间后evit的问题,因此当cache中无相应状态时,需要从数据库中读取并存入cache。

结合公司项目实例,采用Redis Cache解决Duplicate check的问题:
前端需要Post一个文章,此时可能存在重复Post的bug或者恶意攻击,后台需要对此进行处理。

boolean isApplied = getCache(null, cacheManager, ApplyStatus_CACHE_NAME,id);//根据Id获取内存中的状态
       if (isApplied) {
                return false;
       }//如果已经发送,则返回false,提示重复发送
if(isApplied==null){        如果cache中无状态则
    getStatusFromDb(id);    从数据库中拿状态数据,
    checkStatus();          判断是否重复Post,如果重复Post则返回False
    putCache(cacheManager, ApplyStatus_CACHE_NAME, id, true);   如果未重复Post,则将cache中状态赋值未true,并继续后续发送逻辑
}

实际公司框架Redis cache暂不支持事务性,因此并不严格的对Duplicate Post进行了Check,由于redis cache处理的快速性,基本能够满足业务要求。上述代码满足了短时间内连续重复请求的处理,即相同请求过来时,仅仅处理第一条,后续均返回false,不进行处理。同时间隔一定时间后,即使cache过期,也可以通过从DB中拿数据进行业务判断。
上述Redis cache的程序为伪代码,实际编程中,可以使用Jedis或者Redssion客户端代码,Redssion支持CAS的原子性操作,避免了竞态问题,内部通过执行lua script实现。