最近一个项目发现手机验证码总是被人盗刷,一秒钟刷了1百多个,很明显这种行为是通过软件自动提交的,自动发帖机原理类似,解决这个问题目前有两个方案。
出现这个问题原因:请求手机验证码Api时没有任何带任何验证,只要请求了手机号正确就执行发送操作,软件或代码很容易伪造请求过程。
解决方案有很多种,可以选择下面一种或几种组合起来使用。
方案1:用户获取手机验证码时候弹出图片验证码,输入后再发送。
优点:增加伪造请求成功的难度,必须输入验证码才可以发送,如果是软件,软件需要有图片验证码识别功能。
缺点:用户体验不好,增加了普通用户的操作步骤。
方案2:增加ip黑名单,即检测请求ip的发送频率,如同一个ip一分钟内请求多少次后屏蔽ip。
缺点:一些软件有主动更换ip的功能,这种方式效果不是很好。
方案3:加上token口令验证
在Api中增加口令验证,即每次请求必须带上token,token验证正确了才执行发送操作。
这个方案为了替代方案1,因为有的公司对前端体验要求极高,增加用户操作步骤后用户体验不好。
这里token可以放在数据库中,也可以放在内存中,个人建议放在内存中,速度快,查询快,至于冗余的问题,可以增加一个BuildDate来保存生成时间。
Token描述类
public class TokenDescriptor
{
///
/// 客户端唯一Id,必须保证唯一性
///
public string ClientId { get; set; }
///
/// token
///
public string Token { get; set; }
///
/// token生成日期
///
public DateTime BuildDate { get; set; }
}
token处理类
public class TokenFactoryBLL
{
private string _ClientId;
private static List _TokenList;
static TokenFactoryBLL()
{
_TokenList=new List();
}
///
///
///
///
public TokenFactoryBLL(HttpRequestBase httpRequestBase)
{
_ClientId=StringHelper.ClientId(httpRequestBase);
ClearExpired();
}
///
/// 可用于远程api
///
///
public TokenFactoryBLL(string clientId)
{
_ClientId=clientId;
ClearExpired();
}
///
/// 生成口令
///
public string Get()
{
if (string.IsNullOrEmpty(_ClientId))
{
return null;
}
string token=Guid.NewGuid().ToString("N");//guid;
TokenDescriptor tokenDescriptor=new TokenDescriptor();
tokenDescriptor.ClientId=_ClientId;
tokenDescriptor.Token=StringHelper.NewGuid();
tokenDescriptor.BuildDate=Convert.ToDateTime(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
_TokenList.Add(tokenDescriptor);
return token;
}
///
/// 验证token
///
///
///
public TipsInfo Validate(string token)
{
TipsInfo tipsInfo=new TipsInfo();
if (!_TokenList.Exists(c=> c.ClientId.Equals(_ClientId, StringComparison.OrdinalIgnoreCase) && c.Token.Equals(token, StringComparison.OrdinalIgnoreCase)))
{
tipsInfo.State=0;
tipsInfo.Msg="token口令验证失败";
}
return tipsInfo;
}
///
/// 移除对应客户端的所有token
///
///
public void Remove()
{
_TokenList.RemoveAll(c=> c.ClientId.Equals(_ClientId, StringComparison.OrdinalIgnoreCase));
}
///
/// 清理过期的token,过期时间10分钟
///
private static void ClearExpired()
{
for (var i=0; i < _TokenList.Count; i++)
{
TokenDescriptor item=_TokenList[i];
DateTime startTime=item.BuildDate;
DateTime endTime=Convert.ToDateTime(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
TimeSpan ts=endTime - startTime;
int minutes=ts.Minutes;
if (minutes>10)
{
_TokenList.RemoveAt(i);
}
}
}
}
附上Helper工具类中clientId的方法:
///
/// 获取并生成客户端唯一Id,保存在cookie中;
///
///
public static string ClientId(HttpRequestBase request) //获取客户端唯一Id
{
if (request==null)
{
return null;
}
string clientId=CookieHelper.Get("_clientId_");
if (string.IsNullOrEmpty(clientId))
{
string guid=Guid.NewGuid().ToString("N");//guid
string ip=StringHelper.GetClientIP().ToString().Replace(".", "").Replace(":", "");
clientId=guid + ip;
CookieHelper.Add("_clientId_", clientId);
}
return clientId;
}
///
/// 获取客户端ip
///
///
public static string GetClientIP()
{
if (System.Web.HttpContext.Current==null) return "127.0.0.1";
string clientIp=System.Web.HttpContext.Current.Request.ServerVariables["HTTP_X_FORWARDED_FOR"];
if (string.IsNullOrEmpty(clientIp))
{
clientIp=System.Web.HttpContext.Current.Request.ServerVariables["REMOTE_ADDR"];
}
if (string.IsNullOrEmpty(clientIp))
{
clientIp=System.Web.HttpContext.Current.Request.UserHostAddress;
}
if (clientIp.IndexOf(",") > 0)
{
clientIp=clientIp.Split(',')[0];
}
if (!StringHelper.IsIp(clientIp))
{
clientIp="127.0.0.1";
}
return clientIp;
}
前端发送方式需要更改,在请求手机验证码的api之前,先第一次请求二手QQ卖号平台获取token,然后带上token验证通过后后再请求api。
这个方案肯定没有验证码的防护效果好,只是增加了伪造请求的步骤,因为现在很多模拟请求的软件都是一次性请求,所以还是能防护大部分的软件。