生成一个有时效性的二维码字符串

Posted 娃都会打酱油了

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了生成一个有时效性的二维码字符串相关的知识,希望对你有一定的参考价值。

这里所谓的时效性二维码,其实指的是扫码出来的字符串,在验证时会进行时效性以及真实性验证,原理呢参考了网上通用的HMAC签名认证机制,如果已经过了有效期,就不能进行下一步的业务操作,当然二维码的业务时效性也不一定需要通过本文的方式实现,但简单来讲,本文的方式应该是通用性比较广的一种方式。

因为该部分并没有太多内容,只是简单的几个类,所以直接上代码。

首先是秘钥QRCodeSecretKey,从设计上该类完成所有业务秘钥的配置与读取

    /// <summary>
    /// 业务秘钥配置
    /// </summary>
    public class QRCodeSecretKey
    
        /// <summary>
        /// 默认秘钥,如果<see cref="SecretKeys"/>中未能找到对应秘钥,则采用该秘钥,如果两者都没找到或者为空字符串,则会产生抛出异常
        /// </summary>
        public string DefaultSecretKey  get; set; 
        /// <summary>
        /// 业务对应的秘钥设置
        /// </summary>
        public IDictionary<string, string> SecretKeys  get; set; 
        /// <summary>
        /// 根据业务标志获取对应秘钥
        /// </summary>
        /// <param name="bizFlag">业务标志</param>
        /// <returns></returns>
        public string GetSecretKey(string bizFlag)
        
            if (string.IsNullOrWhiteSpace(bizFlag))
            
                throw new ArgumentException(nameof(bizFlag));
            
            string secretKey = null;
            if (this.SecretKeys != null && this.SecretKeys.ContainsKey(bizFlag))
            
                secretKey = this.SecretKeys[bizFlag];
            
            if (string.IsNullOrWhiteSpace(secretKey))
            
                secretKey = this.DefaultSecretKey;
            
            if (string.IsNullOrWhiteSpace(secretKey))
            
                throw new KeyNotFoundException(bizFlag);
            
            return secretKey;
        
    

接下来是验证结果相关的一些类定义

    public class QRCodeValidResult
    
        /// <summary>
        /// 验证结果
        /// </summary>
        public QRCodeValid ValidResult  get; set; 
        /// <summary>
        /// 业务标志
        /// </summary>
        public string BizFlag  get; set; 
        /// <summary>
        /// 业务Key
        /// </summary>
        public string UniqueKey  get; set; 
    

    public enum QRCodeValid
    
        /// <summary>
        /// 不符合的验证码
        /// </summary>
        Error = 0,
        /// <summary>
        /// 验证正确
        /// </summary>
        Correct = 1,
        /// <summary>
        /// 过期
        /// </summary>
        Expired = 2,
    

最后就是时效性辅助类,包括二维码的生成以及验证,既然支持时效性,当然也可以支持永久有效的二维码,代码中用到的HashSignatureHelperMD5Helper分别对应HashSignatureHelper.csHashAlgorithmHelper.cs,验证通过(QRCodeValidResult.ValidResultQRCodeValid.Correct)之后你就可以通过QRCodeValidResult.BizFlagQRCodeValidResult.UniqueKey来进行后续的业务处理了

    /// <summary>
    /// 二维码时效性帮助类
    /// </summary>
    public class QRCodeTimelinessHelper
    
        private int signatureLength = 5;
        private readonly QRCodeSecretKey secretKey;
        /// <summary>
        /// 用于二维码字符串分割的字符,默认为.
        /// </summary>
        public char SplitChar  get; set;  = '.';
        /// <summary>
        /// 用于生成签名的哈希算法,默认为SHA1,其余支持MD5、SHA256、SHA384、SHA512,如果输入不为上述内容,则采用SHA1生成签名
        /// </summary>
        public string HashAlgorithm  get; set;  = "SHA1";
        /// <summary>
        /// 保留的签名长度,默认5,支持的最小值为3,最大值为8
        /// </summary>
        public int SignatureLength
        
            get
            
                return this.signatureLength;
            
            set
            
                if (value < 3)
                
                    throw new ArgumentException("The minimum length supported is 3");
                
                if (value > 8)
                
                    throw new ArgumentException("The maximum length supported is 8");
                
                this.signatureLength = value;
            
        

        /// <summary>
        /// 二维码时效性帮助类
        /// </summary>
        /// <param name="secretKey"></param>
        public QRCodeTimelinessHelper(QRCodeSecretKey secretKey)
        
            this.secretKey = secretKey ?? throw new ArgumentNullException(nameof(secretKey));
        

        /// <summary>
        /// 生成二维码字符串
        /// </summary>
        /// <param name="bizFlag">业务标志</param>
        /// <param name="uniqueKey">业务Key</param>
        /// <param name="effectiveTime">有效时间,为null或<see cref="TimeSpan.Zero"/>表示永久有效</param>
        /// <returns></returns>
        public string GenerateQRCode(string bizFlag, string uniqueKey, TimeSpan? effectiveTime = null)
        
            ValidInputEmptyOrHasSpecialChar(bizFlag, nameof(bizFlag));
            ValidInputEmptyOrHasSpecialChar(uniqueKey, nameof(uniqueKey));
            if (effectiveTime != null && effectiveTime < TimeSpan.Zero)
            
                throw new ArgumentException("effectiveTime must great than 'TimeSpan.Zero'");
            
            var timestamp = GetTimestamp(effectiveTime);
            return $"bizFlagSplitCharuniqueKeySplitChartimestampSplitCharCreateSign(bizFlag, uniqueKey, timestamp)";
        

        /// <summary>
        /// 验证二维码字符串是否正确
        /// </summary>
        /// <param name="code">输入的验证码</param>
        /// <returns></returns>
        public QRCodeValidResult ValidQRCode(string code)
        
            var result = new QRCodeValidResult();
            if (!string.IsNullOrWhiteSpace(code))
            
                var tmpArr = code.Split(SplitChar, StringSplitOptions.RemoveEmptyEntries);
                if (tmpArr.Length == 4)
                
                    // tmpArr[0]--bizFlag  tmpArr[1]--uniqueKey tmpArr[2]--timestamp  tmpArr[3]--sign
                    if (long.TryParse(tmpArr[2], out long timestamp) && timestamp >= 0)
                    
                        try
                        
                            var sign = CreateSign(tmpArr[0], tmpArr[1], timestamp);
                            if (string.Equals(sign, tmpArr[3]))
                            
                                result.ValidResult = QRCodeValid.Correct;
                                if (timestamp > 0)
                                
                                    var dt = GetDateTimeOffset(timestamp);
                                    if (dt < DateTimeOffset.UtcNow)
                                    
                                        result.ValidResult = QRCodeValid.Expired;
                                    
                                
                            
                        
                        catch
                        
                            result.ValidResult = QRCodeValid.Error;
                        
                        if (result.ValidResult == QRCodeValid.Correct)
                        
                            result.BizFlag = tmpArr[0];
                            result.UniqueKey = tmpArr[1];
                        
                    
                
            
            return result;
        

        private string CreateSign(string bizFlag, string uniqueKey, long timestamp)
        
            var str = $"bizFlaguniqueKeysecretKey.GetSecretKey(bizFlag)timestamp";
            var data = HashSignatureHelper.SignData(Encoding.UTF8.GetBytes(str), HashAlgorithm);
            var tmpSign = MD5Helper.ConvertToString(data, false);
            if (tmpSign.Length < signatureLength)
            
                return tmpSign;//如果采用的签名生成的字符串长度小于设置的签名长度,则直接返回
            
            //生成的签名进行截取
            var sIdx = GetStartIndex(bizFlag);
            sIdx = sIdx % tmpSign.Length;
            var subLength = tmpSign.Length - sIdx;
            if (subLength > SignatureLength)
            
                subLength = SignatureLength;
            
            if (subLength < SignatureLength)
            
                return tmpSign.Substring(sIdx, subLength) + tmpSign.Substring(0, SignatureLength - subLength);
            
            else
            
                return tmpSign.Substring(sIdx, subLength);
            
        

        /// <summary>
        /// 将字符串转化为截取字符串的起始位置,注意需要保证生成的数字不会因为不同进程而不同
        /// </summary>
        /// <param name="bizFlag"></param>
        /// <returns></returns>
        protected virtual int GetStartIndex(string bizFlag)
        
            var num = 0;
            foreach (var b in Encoding.UTF8.GetBytes(bizFlag))
            
                unchecked
                
                    num += b;
                
            
            return Math.Abs(num);
        

        /// <summary>
        /// 将TimeSpan转化为Utc时间戳,默认转化为时间戳秒
        /// </summary>
        /// <param name="effectiveTime"></param>
        /// <returns></returns>
        protected virtual long GetTimestamp(TimeSpan? effectiveTime)
        
            if (effectiveTime > TimeSpan.Zero)
            
                var dt = DateTimeOffset.Now + effectiveTime.Value;
                return dt.ToUnixTimeSeconds();
            
            return 0;
        

        /// <summary>
        /// 将UTC时间戳转化为DateTimeOffset,默认timestamp为时间戳秒
        /// </summary>
        /// <param name="timestamp">时间戳</param>
        /// <returns></returns>
        protected virtual DateTimeOffset GetDateTimeOffset(long timestamp)
         
            return DateTimeOffset.FromUnixTimeSeconds(timestamp);
        

        private void ValidInputEmptyOrHasSpecialChar(string input, string name)
        
            if (string.IsNullOrWhiteSpace(input) || input.IndexOf(SplitChar) >= 0)
            
                throw new ArgumentException($"name is empty or contains 'SplitChar'");
            
        
    

测试代码如下

            var dic = new Dictionary<string, TimeSpan?>()
            
                 "Biz1", null,
                 "Biz2",TimeSpan.FromSeconds(2),
                 "Biz3", TimeSpan.Zero,
                 "Biz4", TimeSpan.FromSeconds(-1),
            ;
            var uniqueKey = "111222333444";
            var secretKey = new QRCodeSecretKey 
                DefaultSecretKey = "123",
                SecretKeys = new Dictionary<string, string>
                
                     "Biz2", "5555!@S",
                
            ;
            var helper = new QRCodeTimelinessHelper(secretKey);
            //helper.GenerateQRCode("1.1", "2.2"); //测试分隔符
            foreach (var kv in dic)
            
                Console.WriteLine($"--- BizFlag:kv.Key  Timestamp:kv.Value?.TotalSeconds ---");
                try
                
                    var code = helper.GenerateQRCode(kv.Key, uniqueKey, kv.Value);
                    Console.WriteLine($"Code string:code");
                    Thread.Sleep(3000);//模拟过期
                    var valid = helper.ValidQRCode(code);
                    Console.WriteLine($"Valid result:valid.ValidResult");
                
                catch (Exception ex)
                
                    Console.WriteLine(ex);
                
                Console.WriteLine("--- End ---");
                Console.WriteLine();
            

            Console.WriteLine($"*** Test input code ***");
            do
            
                var code = Console.ReadLine();
                var valid = helper.ValidQRCode(code);
                Console.WriteLine($"Input:code  Valid Result:valid.ValidResult");
            
            while (true);

测试结果图

2021-12-17调整:因为C#GetHashCode方法只保证当前进程不变,最新代码获取截取位置的代码已调整该部分,下图为同样的字符串在不同的进程中GetHashCode执行结果

以上是关于生成一个有时效性的二维码字符串的主要内容,如果未能解决你的问题,请参考以下文章

生成一个有时效性的二维码字符串

生成一个有时效性的二维码字符串

java如何实现二维码签到功能(二维码可以动态设置时效)

流式计算优化:时效性

java web 验证码生成后一般在啥地方保存这个验证码?存到数据库还是怎么地?

从 startup.cs 检查令牌有效性