• 短信验证码验证机制 服务端独立接口实现


    在日常业务场景中,有很多安全性操作例如密码修改、身份认证等等类似的业务,需要先短信验证通过再进行下一步。

    一种直接的方案是提供2个接口:

    1.SendActiveCodeFor密码修改,发送相应的短信+验证Code。

    2.VerifyActiveCodeFor密码修改,参数带入手机接收到的短信验证Code,服务端进行验证,验证成功则开发 修改密码。

    这种方案有一个缺点,即针对大量类似的业务,会出现非常多的SendMessageForXXX+VerifyMessageCodeForXXX这种组合接口,造成非常大的维护负担。

    那么我们是否可以将短信验证码业务独立出来作为一个公用服务呢?

    答:Yes!考虑只有一个 SendActiveCode接口和VerifyActiveCode,验证完成后返回一个token。具体的业务场景去拿这个token来作为判断验证码是否验证通过,来决定进行下一步业务逻辑操作。

    为了业务逻辑完整性,我们还将加入一些短信发送安全性的考虑。(随便网上找了个在线制图,没想到有水印啊~~,,请忽略。)

    主要有以下几个核心逻辑点。

    安全性验证

    主要为了防止短信滥发的情况出现,会针对手机号和手机设备号(能够标识手机唯一性的码)作一些检查限制。

    • 限制同一手机号发送次数,例如每天对多发送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);
            }
        }
  • 相关阅读:
    JS 知识点补充
    JS 数据之间类型的转化
    JS 数据的类型
    数据结构--数组、单链表和双链表介绍 以及 双向链表
    数据结构--队列
    数据结构--栈
    24. 两两交换链表中的节点
    23. 合并K个排序链表
    22. 括号生成
    21. 合并两个有序链表
  • 原文地址:https://www.cnblogs.com/gt1987/p/12728541.html
Copyright © 2020-2023  润新知