web请求重放|幂等性问题

数据库保存相同的多条记录,一般从三个方面进行加固

1、端和h5做事件重复触发

缺点:各种机型和浏览器等兼容性问题会导致部分漏网之鱼,还有接口工具可能直接并发绕过常规

2、服务端做防重放处理,本次记录是服务端处理

3、数据库做唯一约束

缺点:相当于重新走了一圈服务的业务逻辑,数据库层面报错体验也不友好,服务端和数据层都增加压力

下面说下服务端的解决方案

1、token

前端+后端调整

前端请求验证后服务端保存token到session或redis里并返回给前端,前端请求时把token带回来由服务端验证,业务流程走完时删除token

2、

后端

用redis,做业务编号和userId存放在redis中,redis中过期时间设置1到2秒释放,同一个业务操作一般重放逼近时间相同,业务流程走完删除key

3、

后端无感方案,有重放时阻塞,等第一个执行完了,第二个去验证第一个执行结果再再返回成功和失败,或者也可以再次执行下,一般情况都是没有新增有就更新的业务逻辑

3.1在拦截器preHandle给redis加锁

3.2在拦截器preHandle判断有锁阻塞,直到获取解锁验证业务是否正常,正常返回成功结果

3.3在拦截器postHandle给redis解锁

java+Lua 原子性,限流

import com.google.common.collect.Lists;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import redis.clients.jedis.Jedis;
@Slf4j
@Component
public class RedisLua {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    /**
     *INCRBY  请求数加1
     */
    String script = "local key = KEYS[1]\n" +
            "local val = tonumber(ARGV[1])\n" +
            "local limit = tonumber(ARGV[2])\n" +
            "local ss = tonumber(ARGV[3])\n" +
            "local current = tonumber(redis.call(\"get\", key) or \"0\")\n" +
            "if current + 1 > limit then\n" +
            "    return 0\n" +
            "else\n" +
            "    redis.call(\"INCRBY\", key, val)\n" +
            "    redis.call(\"expire\", key, ss)\n" +
            "end\n" +
            "return 1";

    /**
     *
     * @param script  lua脚本
     * @param key     key
     * @param value     value
     * @param count     限流次数
     * @param expire    超时时间:单位:秒
     * @throws Exception
     */
    public Object eavl(String script,String key,String value,int count,int expire){
        try{
            Object ret = stringRedisTemplate.execute(new RedisCallback() {
                public Object doInRedis(RedisConnection connection) {
                    Jedis jedis = (Jedis) connection.getNativeConnection();
                    return jedis.eval(script, Lists.newArrayList(key),Lists.newArrayList(value,String.valueOf(count),String.valueOf(expire)));
                }
            }, true);
            return ret;
        }catch (Exception ex){
            log.error("幂等性异常,不做拦截",ex);
        }
        return null;
    }

    /**
     *
     * @param key     key
     * @param value     value
     * @param count     限流次数
     * @param expire    超时时间:单位:秒
     * @throws Exception
     */
    public Object eavl(String key,String value,int count,int expire){
        return this.eavl(script,key,value,count,expire);
    }
}

拦截器做重放处理

@Slf4j
public class AuthInterceptor implements HandlerInterceptor {
@Autowired
RedisLua redisLua;
String idempotenPrefix = "idempotent";
@Override
public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object o) throws Exception {
 
String mehtod = req.getMethod();
String lastPath = req.getServletPath().substring(req.getServletPath().lastIndexOf("/")+1);//最path中最后一个地址,如果这个没办法排除连续唯一性可以把全路径都用'/'替换成":"做为redis的key
if("post".equals(mehtod.toLowerCase())){//只做post请求
    if (!checkIdempotent(user.getUid(),lastPath)){//flowStep一个步骤可能有多个post请求
        log.warn("idempoten user {},path {}",userId,req.getServletPath());
        log.warn("idempoten expire second:"+redisLua.getExpire(idempotenPrefix+":"+lastPath+":"+userId));
        //resp.getWriter().write();这里可以做前端提示或无感重复提交返回成功后的跳转
        return false;
    }
}
return true;

/**
 * 重放,幂等性问题拦截
 * @param userId
 * @return
 */
private boolean checkIdempotent(Long userId,String step) {
    String key = idempotenPrefix+":"+step+":"+userId;
    Object  ret = redisLua.eavl(key,"1",1,1);
    if (null == ret){//redis异常不拦截
        return true;
    }
    if (1 == Integer.valueOf(ret.toString())){//正常
        return true;
    }
    return false;
}

}

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值