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

这篇博客介绍了如何实现二维码的时效性验证和生成,主要利用HMAC签名认证机制,确保二维码在有效期内真实可用。内容包括QRCodeSecretKey类用于管理业务密钥,QRCodeTimelinessHelper类用于生成和验证二维码,以及相关验证结果类QRCodeValidResult。代码示例展示了如何生成和验证二维码,并处理时效性问题。
摘要由CSDN通过智能技术生成

这里所谓的时效性二维码,其实指的是扫码出来的字符串,在验证时会进行时效性以及真实性验证,原理呢参考了网上通用的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 $"{bizFlag}{SplitChar}{uniqueKey}{SplitChar}{timestamp}{SplitChar}{CreateSign(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 = $"{bizFlag}{uniqueKey}{secretKey.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执行结果在这里插入图片描述

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值