在平常业务场景中,有不少安全性操做例如密码修改、身份认证等等相似的业务,须要先短信验证经过再进行下一步。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); } }