在平常業務場景中,有不少安全性操做例如密碼修改、身份認證等等相似的業務,須要先短信驗證經過再進行下一步。git
一種直接的方案是提供2個接口:算法
1.SendActiveCodeFor密碼修改,發送相應的短信+驗證Code。json
2.VerifyActiveCodeFor密碼修改,參數帶入手機接收到的短信驗證Code,服務端進行驗證,驗證成功則開發 修改密碼。api
這種方案有一個缺點,即針對大量相似的業務,會出現很是多的SendMessageForXXX+VerifyMessageCodeForXXX這種組合接口,形成很是大的維護負擔。緩存
那麼咱們是否能夠將短信驗證碼業務獨立出來做爲一個公用服務呢?安全
答:Yes!考慮只有一個 SendActiveCode接口和VerifyActiveCode,驗證完成後返回一個token。具體的業務場景去拿這個token來做爲判斷驗證碼是否驗證經過,來決定進行下一步業務邏輯操做。分佈式
爲了業務邏輯完整性,咱們還將加入一些短信發送安全性的考慮。(隨便網上找了個在線製圖,沒想到有水印啊~~,,請忽略。)ui
主要有如下幾個核心邏輯點。加密
主要爲了防止短信濫發的狀況出現,會針對手機號和手機設備號(可以標識手機惟一性的碼)做一些檢查限制。spa
該token主要是爲了在VerifyActiveCode接口能正確獲取第一步SendActiveCode接口中的一些數據用於驗證。這些數據不能直接經過VerifyActiveCode接口帶入!不然對於服務端接口,會有跳過第一步接口,直接調用第二個接口驗證的漏洞。
經過token可以獲取的內容應當至少包括如下:
那麼對從token如何獲取內容也有2種方案,各有千秋
前者安全性更高,可是強依賴緩存依賴;後者更加獨立無依賴,可是加密算法要夠強,加密密鑰須要嚴加保密。一旦加密被破解,會產生嚴重的安全問題。
該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); } }