最近一個項目發現手機驗證碼總是被人盜刷,一秒鐘刷了1百多個,很明顯這種行為是通過軟件自動提交的,自動發帖機原理類似,解決這個問題目前有兩個方案。
出現這個問題原因:請求手機驗證碼Api時沒有任何帶任何驗證,只要請求了手機號正確就執行發送操作,軟件或代碼很容易偽造請求過程。
解決方案有很多種,可以選擇下面一種或幾種組合起來使用。
方案1:用戶獲取手機驗證碼時候彈出圖片驗證碼,輸入后再發送。
優點:增加偽造請求成功的難度,必須輸入驗證碼才可以發送,如果是軟件,軟件需要有圖片驗證碼識別功能。
缺點:用戶體驗不好,增加了普通用戶的操作步驟。
方案2:增加ip黑名單,即檢測請求ip的發送頻率,如同一個ip一分鐘內請求多少次后屏蔽ip。
缺點:一些軟件有主動更換ip的功能,這種方式效果不是很好。
方案3:加上token口令驗證
在Api中增加口令驗證,即每次請求必須帶上token,token驗證正確了才執行發送操作。
這個方案為了替代方案1,因為有的公司對前端體驗要求極高,增加用戶操作步驟后用戶體驗不好。
這里token可以放在數據庫中,也可以放在內存中,個人建議放在內存中,速度快,查詢快,至于冗余的問題,可以增加一個BuildDate來保存生成時間。
Token描述類
public class TokenDescriptor
{
/// <summary>
/// 客戶端唯一Id,必須保證唯一性
/// </summary>
public string ClientId { get; set; }
/// <summary>
/// token
/// </summary>
public string Token { get; set; }
/// <summary>
/// token生成日期
/// </summary>
public DateTime BuildDate { get; set; }
}
token處理類
public class TokenFactoryBLL
{
private string _ClientId;
private static List<TokenDescriptor> _TokenList;
static TokenFactoryBLL()
{
_TokenList = new List<TokenDescriptor>();
}
/// <summary>
///
/// </summary>
/// <param name="httpRequestBase"></param>
public TokenFactoryBLL(HttpRequestBase httpRequestBase)
{
_ClientId = StringHelper.ClientId(httpRequestBase);
ClearExpired();
}
/// <summary>
/// 可用于遠程api
/// </summary>
/// <param name="clientId"></param>
public TokenFactoryBLL(string clientId)
{
_ClientId = clientId;
ClearExpired();
}
/// <summary>
/// 生成口令
/// </summary>
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;
}
/// <summary>
/// 驗證token
/// </summary>
/// <param name="token"></param>
/// <returns></returns>
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;
}
/// <summary>
/// 移除對應客戶端的所有token
/// </summary>
/// <param name="clientId"></param>
public void Remove()
{
_TokenList.RemoveAll(c => c.ClientId.Equals(_ClientId, StringComparison.OrdinalIgnoreCase));
}
/// <summary>
/// 清理過期的token,過期時間10分鐘
/// </summary>
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的方法:
/// <summary>
/// 獲取并生成客戶端唯一Id,保存在cookie中;
/// </summary>
/// <returns></returns>
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;
}
/// <summary>
/// 獲取客戶端ip
/// </summary>
/// <returns></returns>
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之前,先第一次請求獲取token,然后帶上token驗證通過后后再請求api。
這個方案肯定沒有驗證碼的防護效果好,只是增加了偽造請求的步驟,因為現在很多模擬請求的軟件都是一次性請求,所以還是能防護大部分的軟件。