短信驗證碼驗證機制 服務端獨立接口實現

在平常業務場景中,有不少安全性操做例如密碼修改、身份認證等等相似的業務,須要先短信驗證經過再進行下一步。git

一種直接的方案是提供2個接口:算法

1.SendActiveCodeFor密碼修改,發送相應的短信+驗證Code。json

2.VerifyActiveCodeFor密碼修改,參數帶入手機接收到的短信驗證Code,服務端進行驗證,驗證成功則開發 修改密碼。api

這種方案有一個缺點,即針對大量相似的業務,會出現很是多的SendMessageForXXX+VerifyMessageCodeForXXX這種組合接口,形成很是大的維護負擔。緩存

那麼咱們是否能夠將短信驗證碼業務獨立出來做爲一個公用服務呢?安全

答:Yes!考慮只有一個 SendActiveCode接口和VerifyActiveCode,驗證完成後返回一個token。具體的業務場景去拿這個token來做爲判斷驗證碼是否驗證經過,來決定進行下一步業務邏輯操做。分佈式

爲了業務邏輯完整性,咱們還將加入一些短信發送安全性的考慮。(隨便網上找了個在線製圖,沒想到有水印啊~~,,請忽略。)ui

主要有如下幾個核心邏輯點。加密

安全性驗證

主要爲了防止短信濫發的狀況出現,會針對手機號和手機設備號(可以標識手機惟一性的碼)做一些檢查限制。spa

  • 限制同一手機號發送次數,例如天天對多發送10次,或者每小時 最多發送5次,等等相似
  • 限制t同一手機號發送頻率,例如每60秒最多發送一次
  • 限制同一手機設備號發送次數,例如天天最多發送20次
  • 限制同一手機號設備號發送頻率,例如每分鐘最多2次
  • 增長手機黑名單和手機設備號機制

接口上下文Token

該token主要是爲了在VerifyActiveCode接口能正確獲取第一步SendActiveCode接口中的一些數據用於驗證。這些數據不能直接經過VerifyActiveCode接口帶入!不然對於服務端接口,會有跳過第一步接口,直接調用第二個接口驗證的漏洞。

經過token可以獲取的內容應當至少包括如下:

  • 手機號,驗證先後是否一致
  • 設備號,驗證先後是否一致
  • Code,第一步接口生成的驗證Code,用於和VerifyActiveCode接口參數傳遞的Code對比驗證
  • 業務ID,標識哪一個業務模塊,可用與獲取短信模板發送
  • 建立時間
  • 過時時間,這個根據具體業務設定,通常5分鐘便可。一個驗證場景差很少就是這個時間跨度

那麼對從token如何獲取內容也有2種方案,各有千秋

  • token爲一個無任何含義的隨機字符串(如Guid),服務端將token內容與token匹配關係存到分佈式緩存中。第一步接口以token爲key從緩存獲取對應內容來驗證。
  • token爲一個有實質內容的加密字符串,服務端接收到token,進行解密獲取內容來驗證。

前者安全性更高,可是強依賴緩存依賴;後者更加獨立無依賴,可是加密算法要夠強,加密密鑰須要嚴加保密。一旦加密被破解,會產生嚴重的安全問題。

驗證成功Token

該token主要是爲了標識驗證結果,沒有什麼敏感性內容。可是須要有能驗籤、防篡改、時效性這些特性。全部jwt是一個很好的選擇。

 

OK,設計部分就講完了,若是對實現有興趣的話,你們能夠從這裏直接下載:https://gitee.com/gt1987/gt.Microservice/tree/master/src/Services/ShortMessage/gt.ShortMessage

這些貼一些關鍵性代碼。

1.安全性驗證模塊,IMessageSendValidator 負責檢查和數據收集統計。注意,負責具體執行的是 IPhoneValidator和IUniqueIdValidator,具體的實現有PhoneBlackListValidator、PhonePerDayCountValidator、UniqueIdPerDayCountValidator。可擴展添加

public class MessageSendValidator : IMessageSendValidator
    {
        private readonly List<IPhoneValidator> _phoneValidators = null;
        private readonly List<IUniqueIdValidator> _uniqueIdValidators = null;
        private readonly ILogger _logger;
        public MessageSendValidator(List<IPhoneValidator> phoneValidators,
            List<IUniqueIdValidator> uniqueIdValidators,
            ILogger<MessageSendValidator> logger)
        {
            _phoneValidators = phoneValidators ?? new List<IPhoneValidator>();
            _uniqueIdValidators = uniqueIdValidators ?? new List<IUniqueIdValidator>();
            _logger = logger;
        }

        public bool Validate(string phone, string uniqueId)
        {
            if (string.IsNullOrEmpty(phone) || string.IsNullOrEmpty(uniqueId)) return false;
            bool result = true;
            foreach (var validator in _phoneValidators)
            {
                if (!validator.Validate(phone))
                {
                    _logger.LogDebug($"phone:{phone} validate failed by {validator.GetType()}");
                    result = false;
                    break;
                }
            }
            if (!result) return result;

            foreach (var validator in _uniqueIdValidators)
            {
                if (!validator.Validate(uniqueId))
                {
                    _logger.LogDebug($"uniqueId:{uniqueId} validate failed by {validator.GetType()}");
                    result = false;
                    break;
                }
            }
            return result;
        }

        public void AfterSend(string phone, string uniqueId)
        {
            if (string.IsNullOrEmpty(phone) || string.IsNullOrEmpty(uniqueId)) return;
            foreach (var validator in _phoneValidators)
            {
                validator.Statistics(phone);
            }

            foreach (var validator in _uniqueIdValidators)
            {
                validator.Statistics(uniqueId);
            }
        }
    }

2.Token模塊,這裏實現的是加密token方式。

    /// <summary>
    /// 加密token
    /// 生成一個加密字符串,用於上下文驗證
    /// 優勢:無狀態,無依賴服務端存儲
    /// 缺點:加密算法要夠強,不然被破解會致使安全問題。
    /// </summary>
    public class EncryptTokenService : ITokenService
    {
        private ILogger _logger;
        private readonly string _tokenSecret = "secret234234287fdf4";
        public EncryptTokenService(ILogger<EncryptTokenService> logger)
        {
            _logger = logger;
        }

        public string CreateSuccessToken(string phone, string uniqueId)
        {
            //這裏嘗試生成一個jwt,沒有敏感信息,主要用於驗證
            var claims = new[] {
                new Claim(ClaimTypes.MobilePhone,phone),
                new Claim("uniqueId",uniqueId),
                new Claim("succ","true")
            };
            var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_tokenSecret));
            var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
            var token = new JwtSecurityToken("www.gt.com", null, claims, null, DateTime.Now.AddMinutes(10), creds);
            return new JwtSecurityTokenHandler().WriteToken(token);
        }

        public string CreateActiveCodeToken(ActiveCode code)
        {
            var json = JsonConvert.SerializeObject(code);
            return SecurityHelper.DesEncrypt(json);
        }

        public bool VerifyActiveCodeToken(string token, string code, ref ActiveCode activeCode)
        {
            string json = string.Empty;
            try
            {
                json = SecurityHelper.DesDecrypt(token);
                activeCode = JsonConvert.DeserializeObject<ActiveCode>(json);
            }
            catch (Exception ex)
            {
                _logger.LogDebug($"token:{token}.error:{ex.Message + ex.StackTrace}");
            }
            if (activeCode == null) return false;
            if (activeCode.ExpiredTimeStamp < DateTimeHelper.ToTimeStamp(DateTime.Now))
            {
                _logger.LogDebug($"token {json} expired.");
                return false;
            }
            if (!string.Equals(activeCode.Code, code, StringComparison.CurrentCultureIgnoreCase))
            {
                _logger.LogDebug($"token {json} code not match {code}.");
                return false;
            }
            return true;
        }
    }

具體的接口code爲

    [Route("api/[controller]")]
    [ApiController]
    public class ShortMessageController : ApiControllerBase
    {
        private readonly IMessageSendValidator _validator;
        private readonly IActiveCodeService _activeCodeService;
        private readonly ITokenService _tokenService;
        private readonly IShortMessageService _shortMessageService;

        public ShortMessageController(IMessageSendValidator validator,
            IActiveCodeService activeCodeService,
            ITokenService tokenService,
            IShortMessageService shortMessageService)
        {
            _validator = validator;
            _activeCodeService = activeCodeService;
            _tokenService = tokenService;
            _shortMessageService = shortMessageService;
        }


        [Route("ping")]
        [HttpGet]
        public IActionResult Ping()
        {
            return Ok("ok");
        }
        /// <summary>
        /// 發送短信驗證碼
        /// </summary>
        /// <param name="request"></param>
        /// <returns></returns>
        [Route("activecode")]
        [HttpPost]
        public IActionResult ActiveCode(SendActiveCodeRequest request)
        {
            if (request == null ||
                string.IsNullOrEmpty(request.Phone) ||
                string.IsNullOrEmpty(request.UniqueId) ||
                string.IsNullOrEmpty(request.BusinessId))
                return BadRequest();

            if (!_validator.Validate(request.Phone, request.UniqueId))
                return Error(-1, "手機號或設備號發送次數受限!");

            var activeCode = _activeCodeService.GenerateActiveCode(request.Phone, request.UniqueId, request.BusinessId);
            var token = _tokenService.CreateActiveCodeToken(activeCode);
            var result = _shortMessageService.SendActiveCode(activeCode.Code, activeCode.BusinessId);

            if (!result)
                return Error(-2, "短信發送失敗,請從新嘗試!");

            _validator.AfterSend(request.Phone, request.UniqueId);

            return Success(token);
        }

        /// <summary>
        /// 短信驗證碼驗證
        /// </summary>
        /// <param name="request"></param>
        /// <returns></returns>
        [Route("verifyActivecode")]
        [HttpPost]
        public IActionResult VerifyActiveCode(VerifyActiveCodeRequest request)
        {
            if (request == null ||
                string.IsNullOrEmpty(request.Code)
                || string.IsNullOrEmpty(request.Token))
                return BadRequest();

            ActiveCode activeCode = null;

            if (!_tokenService.VerifyActiveCodeToken(request.Token, request.Code, ref activeCode))
                return Error(-5, "驗證失敗!");

            //返回驗證成功的token,用於後續處理業務。token應有 可驗籤、防篡改、時效性特徵。這裏jwt比較適合
            var successToken = _tokenService.CreateSuccessToken(activeCode.Phone, activeCode.UniqueId);
            return Success(successToken);
        }
    }
相關文章
相關標籤/搜索