第十节:幂等性-删token方案优化升级方案实操

一.  背景

1 思考1

 秒杀场景,进入详情页就获取token,会产生大量的获取token请求,如何扛住呢?

 解决方案:之前在“方案1”中提到过,在详情页的下一个页面,即“确认付款”页面获取token,这样可以屏蔽掉很多只浏览商品,但是不下单的用户, 大大降低了获取token的数量。

2 思考2

 上述方案1,无论在哪个页面获取token,针对同一个用户,只要进入几次,就会获取几次token,还是会产生大量token,也要防止不法分子恶意频繁进入的问题。

 解决方案:我们通过限制,让同一个用户针对同一个商品只能获取一个token,使用一次就失效

3 思考3

 Token如何防止伪造呢?

 解决方案:返回给前端的token是加密后的,没有任何逻辑可言

 

二. 方案实操说明

1. 流程图

如下:

image

 

2  获取token

  A. 前端需要传递sense 和 key 两个参数,分别代表:类型标记、商品Id

  B. 服务端校验key 和 sense的合法性

  C. 服务端构建tokenKey,形如:tokenKey = $"token:{sense}:{userId}:{goodId}";

     构建加密前tempTokenValue,形如:tempTokenValue = $"token:{sense}:{userId}:{goodId}:{Guid.NewGuid():N}";

  D. 对tempTokenValue采用DES算法进行加密,形成最终的tokenValue

  E. 将tokenKey和tokenValue写入redis,并将tokenValue返回给前端

前端代码:

            //封装获取Token的方法
            function initToken() {
                $.post("/Demo9/GetToken4", { sense: "buy", key: "goodId_002" }, function (res) {
                    console.log(res);
                    let { status, msg, data } = res;
                    if (status == "ok") {
                        window.localStorage.setItem("md_token4", data);
                    }
                });
            }

服务端获取token代码 

  /// <summary>
  /// 02-获取Token
  /// </summary>
  /// <param name="sense">一个标记,比如下单的token此处标记为buy</param>
  /// <param name="key">商品id</param>
  /// <returns></returns>
  [HttpPost]
  public IActionResult GetToken4(string sense, string key)
  {
      try
      {
          //1. 校验key的合法性
          // 检查下key是不是合法的值(存在的商品id),如果不合法,拒绝生成token,避免攻击者传入一堆随机的key来生成token。
          //1.1 先采用布隆过滤器快速判错
          //1.2 布隆过滤器中判断存在,然后DB中校验一下
          //这里面临时模拟一下
          List<string> keyList = ["goodId_001", "goodId_002", "goodId_003", "goodId_004"];
          if (!keyList.Contains(key))
          {
              return Json(new { status = "error", msg = "获取失败,非法参数key" });
          }
          //2. sense的枚举校验
          List<string> secseList = ["buy", "commonBuy", "buyBob"];   //代表不同的购买类别
          if (!secseList.Contains(sense))
          {
              return Json(new { status = "error", msg = "获取失败,非法参数sense" });
          }
          //3. 生成tokenKey和tokenValue
          string userId = "userId_0001";   //正常应该从jwt的token中获取,这里固定写死
          string goodId = key;
          string tokenKey = $"token:{sense}:{userId}:{goodId}";
          var tempTokenValue = $"token:{sense}:{userId}:{goodId}:{Guid.NewGuid():N}";
          string tokenValue = SecurityHelp.DESEncrypt(tempTokenValue, config["SecretKey"]);
          //4.存入redis并将tokenValue返回前端(10min过期)
          RedisHelper.Set(tokenKey, tokenValue, 60 * 10);

          return Json(new { status = "ok", msg = "获取成功", data = tokenValue });
      }
      catch (Exception)
      {
          return Json(new { status = "error", msg = "获取失败", data = "" });
      }
  }

 

3 下单时前置token校验

  A. 前端携带获取的token(即:tokenValue)和goodId,调用下单接口

  B. 对token进行非空校验

  C. 对用token进行DES解密,然后截取获得tokenKey 和 goodId

  D. 判断截取获得的goodId与传递过来的goodId是否一致,不一致则为非法请求 【token只能针对特定的商品进行校验下单!!!】

  E. 利用lua脚本进行下面逻辑

     a. 根据tokenKey去redis中获取value值(可能是空的)

     b. 判断这个value和传递的token是否相等,不相等则为重复请求(或非法请求)

     c. 如果相等,则走后面删除tokenKey的逻辑

  F. 校验完成

前端代码:

          $('#j_btn1').click(() => {
              let token = window.localStorage.getItem("md_token4")
              $.post("/Demo9/PutOrder4", { token, goodId: 'goodId_002' }, function (res) {
                  let { status, msg, data } = res;
                  //表示下单成功,可以给用户提示
                  if (status == "ok") {
                      $('#j_msg').html(msg);
                      console.log(msg);
                  }
                  //表示检验失败,需要给用户提示
                  if (status == "error") {
                      $('#j_msg').html(msg);
                      console.log(msg);
                  }
                  //表示是重复请求,不用给用户提示,这里打印一下即可
                  if (status == "error1") {
                      console.log(msg);
                  }
                  //表示业务执行失败,需要给用户提示,然后偷偷重新获取一下token
                  if (status == "error2") {
                      $('#j_msg').html(msg);
                      console.log(msg);
                      //需要重新获取token
                      initToken();
                  }
              });
          });

 

服务端代码: 

 /// <summary>
 /// 03-下单
 /// </summary>
 /// <param name="token">前端传递加密后的token</param>
 /// <param name="goodId">商品Id</param>
 /// <returns></returns>
 [HttpPost]
 public IActionResult PutOrder4(string token, string goodId)
 {
     //1.幂等性校验
     try
     {
         //1.1 非空校验
         if (string.IsNullOrEmpty(token)) { return Json(new { status = "error", msg = "非法请求,token为空" }); }
         //1.2 从加密的token中解密出tokenKey
         var decTokenValue = SecurityHelp.DESDecrypt(token, config["SecretKey"]);   //解密后的tokenValue
         var tokenKey = decTokenValue.Substring(0, decTokenValue.LastIndexOf(':'));
         //1.3 校验token对应的商品和下单的商品是否是同一个
         var token_goodId = tokenKey.Split(":")[3];
         if (token_goodId != goodId)
         {
             return Json(new { status = "error", msg = "非法请求,token非法" });
         }
         //1.4 使用该tokenKey去redis中获取value,然后和传递过来的token进行比较
         string luaScript = """
             local value = redis.call('GET', KEYS[1])    
             if value ~= ARGV[1] then
                 return 'error'
             end
             redis.call('DEL', KEYS[1])
             return 'ok'
             """;
         var result = RedisHelper.Eval(luaScript, tokenKey, [token]);
         if ((string)result == "error") 
         {
             return Json(new { status = "error1", msg = "重复请求,下单失败" });
         }
     }
     catch (Exception)
     {
         return Json(new { status = "error", msg = "token校验失败" });
     }
     //2. 模拟下单逻辑
     try
     {
         //int.Parse("sdfsdf"); //模拟DB出错
         //仅测试扣减库存,高并发问题不再该章节处理
         FormattableString sql1 = $"update T_Stock set productStock=productStock-1 where productId=10001";
         int count2 = db.Database.ExecuteSqlInterpolated(sql1);
         return Json(new { status = "ok", msg = "下单成功"});
     }
     catch (Exception)
     {
         //表示token删除成功,DB业务执行失败,需要重新获取token进行下单
         return Json(new { status = "error2", msg = "业务执行失败,需要重新走下单流程" });
     }
 }

 

 4 下单扣减库存

   简单模拟即可

 

 

 

 

 

 

 

 

!

  • 作       者 : Yaopengfei(姚鹏飞)
  • 博客地址 : http://www.cnblogs.com/yaopengfei/
  • 声     明1 : 如有错误,欢迎讨论,请勿谩骂^_^。
  • 声     明2 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。
 
posted @ 2025-11-07 07:36  Yaopengfei  阅读(9)  评论(1)    收藏  举报