第十节:幂等性-删token方案优化升级方案实操
一. 背景
1 思考1
秒杀场景,进入详情页就获取token,会产生大量的获取token请求,如何扛住呢?
解决方案:之前在“方案1”中提到过,在详情页的下一个页面,即“确认付款”页面获取token,这样可以屏蔽掉很多只浏览商品,但是不下单的用户, 大大降低了获取token的数量。
2 思考2
上述方案1,无论在哪个页面获取token,针对同一个用户,只要进入几次,就会获取几次token,还是会产生大量token,也要防止不法分子恶意频繁进入的问题。
解决方案:我们通过限制,让同一个用户针对同一个商品只能获取一个token,使用一次就失效
3 思考3
Token如何防止伪造呢?
解决方案:返回给前端的token是加密后的,没有任何逻辑可言
二. 方案实操说明
1. 流程图
如下:

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 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。

浙公网安备 33010602011771号