生成一个有时效性的二维码字符串
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,
最后就是时效性辅助类,包括二维码的生成以及验证,既然支持时效性,当然也可以支持永久有效的二维码,代码中用到的HashSignatureHelper
和MD5Helper
分别对应HashSignatureHelper.cs和HashAlgorithmHelper.cs,验证通过(QRCodeValidResult.ValidResult
为QRCodeValid.Correct
)之后你就可以通过QRCodeValidResult.BizFlag
和QRCodeValidResult.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
执行结果
以上是关于生成一个有时效性的二维码字符串的主要内容,如果未能解决你的问题,请参考以下文章