zoukankan      html  css  js  c++  java
  • 短信验证码验证机制 服务端独立接口实现

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

    一种直接的方案是提供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);
            }
        }
  • 相关阅读:
    防删没什么意思啊,直接写废你~
    绝大多数情况下,没有解决不了的问题,只有因为平时缺少练习而惧怕问题的复杂度,畏惧的心理让我们选择避让,采取并不那么好的方案去解决问题
    Java 模拟面试题
    Crossthread operation not valid: Control 'progressBar1' accessed from a thread other than the thread it was created on
    一步步从数据库备份恢复SharePoint Portal Server 2003
    【转】理解 JavaScript 闭包
    Just For Fun
    The database schema is too old to perform this operation in this SharePoint cluster. Please upgrade the database and...
    Hello World!
    使用filter筛选刚体碰撞
  • 原文地址:https://www.cnblogs.com/gt1987/p/12728541.html
Copyright © 2011-2022 走看看