10

我想在我的应用中为客户打印发票。每张发票都有一个发票 ID。我希望 ID 为:

  • 顺序(最近输入的 ID 来晚了)
  • 32 位整数
  • 像 1 2 3 这样不容易追踪,因此人们无法分辨我们销售了多少商品。

我自己的一个想法:自特定日期和时间(例如 1/1/2010 00 AM)以来 的秒数。

还有其他想法如何生成这些数字吗?

4

10 回答 10

10

我不喜欢使用时间的想法。你可能会遇到各种各样的问题——时差、在一秒钟内发生的几个事件等等。

如果您想要一些连续且不易追踪的东西,那么如何为每个新 ID 生成一个介于 1 和您想要的任何值(例如 100)之间的随机数。每个新的 Id 将是前一个 Id + 随机数。

您还可以为您的 ID 添加一个常量,以使它们看起来更令人印象深刻。例如,您可以将 44323 添加到所有 ID,并将 ID 15、23 和 27 转换为 44338、44346 和 44350。

于 2013-08-10T06:44:55.253 回答
7

你的问题有两个问题。一个是可解决的,一个不是(在你给出的约束下)。

可解:不可猜测的数字

第一个非常简单:当客户可以访问一组有效的发票号码时,客户应该很难猜出一个有效的发票号码(或下一个有效的发票号码)。

你可以用你的约束来解决这个问题:

将您的发票号码分成两部分:

  1. 一个 20 位前缀,取自一系列递增的数字(例如自然数 0,1,2,...)
  2. 随机生成的 10 位后缀

使用这些方案,大约有 100 万个有效发票号码。您可以预先计算它们并将它们存储在数据库中。当出现发票编号时,请检查它是否在您的数据库中。如果不是,则无效。

使用 SQL 序列来分发数字。当发出新的(即未使用的)发票编号时,增加序列并从预先计算的列表中发出第 n 个编号(按价值排序)。

无法解决:猜测客户数量

当您想防止拥有多个有效发票编号的客户猜测您已经开具了多少发票编号(以及您拥有多少客户)时:这是不可能的。

你有一个变种形式,即所谓的“德国坦克问题”。二战时,盟军用印在德国坦克齿轮箱上的序列号来推测德国生产了多少坦克。这行得通,因为序列号不断增加。

但即使你增加了有差距的数字,德国坦克问题的解决方案仍然有效。这很容易:

  1. 您使用此处描述的方法来猜测开出的最高发票编号
  2. 您猜测两个连续发票编号之间的平均差,并将该数字除以该值
  3. 您可以使用线性回归来获得稳定的 delta 值(如果存在)。

现在您可以很好地猜测发票数量的数量级(200、15000、半百万等)。

只要(理论上)存在两个连续发票编号的平均值,这就会起作用。这通常是这种情况,即使在使用随机数生成器时也是如此,因为大多数随机数生成器都被设计为具有这样的平均值。

有一个对策:您必须确保两个连续数字的差距不存在平均值。具有此属性的随机数生成器可以很容易地构建。

例子:

  1. 以最后一个发票编号加一作为当前编号开始
  2. 将当前数乘以 >=2 的随机数。这是您当前的新号码。
  3. 获取一个随机位:如果该位为 0,则结​​果是您当前的数字。否则返回步骤 2。

虽然这在理论上可行,但您很快就会用完 32 位整数。

我认为这个问题没有实际的解决方案。两个连续数字之间的差距有一个平均值(几乎没有变化),您可以轻松猜测发行数字的数量。否则您将很快用完 32 位数字。

蛇油(非工作解决方案)

不要使用任何基于时间的解决方案。时间戳通常很容易猜到(可能会在发票的某处打印一个近似正确的时间戳)。使用时间戳通常会使攻击者更容易,而不是更难。

不要使用不安全的随机数。大多数随机数生成器在密码学上都不是安全的。它们通常具有对统计有利但对您的安全不利的数学属性(例如,可预测的分布、稳定的平均值等)

于 2013-08-15T13:10:39.877 回答
5

一种解决方案可能涉及异或 (XOR) 二进制位图。结果函数是可逆的,可能会产生不连续的数字(如果最低有效字节的第一位设置为1),并且非常容易实现。而且,只要您使用可靠的序列生成器(例如您的数据库),就不需要担心线程安全问题。

根据 MSDN,“当且仅当其操作数之一为真时,[异或运算]的结果为真。” 反向逻辑说相等的操作数总是会导致错误。

例如,我刚刚在 Random.org 上生成了一个 32 位序列。就是这个:

11010101111000100101101100111101

这个二进制数转换为十进制的3588381501 ,十六进制的0xD5E25B3D。我们称它为您的基本密钥

现在,让我们使用([base key] XOR [ID])公式生成一些值。在 C# 中,这就是您的加密函数的样子:

    public static long FlipMask(long baseKey, long ID)
    {
        return baseKey ^ ID;
    }

以下列表包含一些生成的内容。其列如下:

  • ID
  • ID的二进制表示
  • 异或运算后的二进制值
  • 最终的“加密”十进制值

    0 | 000 | 11010101111000100101101100111101 | 3588381501
    1 | 001 | 11010101111000100101101100111100 | 3588381500
    2 | 010 | 11010101111000100101101100111111 | 3588381503
    3 | 011 | 11010101111000100101101100111110 | 3588381502
    4 | 100 | 11010101111000100101101100111001 | 3588381497
    

为了反转生成的键并确定原始值,您只需要使用相同的基本键进行相同的异或运算。假设我们要获取第二行的原始值:

    11010101111000100101101100111101 XOR
    11010101111000100101101100111100 =
    00000000000000000000000000000001

这确实是你的原始价值。

现在,Stefan提出了很好的观点,第一个话题很关键。

为了解决他的顾虑,您可以保留最后一个,比如说,8 个字节作为纯随机垃圾(我相信它被称为nonce),您在加密原始 ID 时生成并在反转它时忽略它。这将大大增加您的安全性,但会牺牲所有可能的 32 位正整数的大量切片(16,777,216 而不是 4,294,967,296,或它的 1/256。)

执行此操作的类如下所示:

public static class int32crypto
{
    // C# follows ECMA 334v4, so Integer Literals have only two possible forms -
    // decimal and hexadecimal.
    // Original key:               0b11010101111000100101101100111101
    public static long baseKey = 0xD5E25B3D;

    public static long encrypt(long value)
    {
        // First we will extract from our baseKey the bits we'll actually use.
        // We do this with an AND mask, indicating the bits to extract.
        // Remember, we'll ignore the first 8. So the mask must look like this:
        // Significance mask:      0b00000000111111111111111111111111
        long _sigMask = 0x00FFFFFF;

        // sigKey is our baseKey with only the indicated bits still true.
        long _sigKey = _sigMask & baseKey;

        // nonce generation. First security issue, since Random()
        // is time-based on its first iteration. But that's OK for the sake
        // of explanation, and safe for most circunstances.
        // The bits it will occupy are the first eight, like this:
        // OriginalNonce:          0b000000000000000000000000NNNNNNNN
        long _tempNonce = new Random().Next(255);

        // We now shift them to the last byte, like this:
        // finalNonce:             0bNNNNNNNN000000000000000000000000
        _tempNonce = _tempNonce << 0x18;

        // And now we mix both Nonce and sigKey, 'poisoning' the original
        // key, like this:

        long _finalKey = _tempNonce | _sigKey;

        // Phew! Now we apply the final key to the value, and return
        // the encrypted value.

        return _finalKey ^ value;


    }

    public static long decrypt(long value)
    {
        // This is easier than encrypting. We will just ignore the bits
        // we know are used by our nonce.
        long _sigMask = 0x00FFFFFF;
        long _sigKey = _sigMask & baseKey;

        // We will do the same to the informed value:
        long _trueValue = _sigMask & value;

        // Now we decode and return the value:
        return _sigKey ^ _trueValue;

    }

}
于 2013-08-17T04:24:11.383 回答
2

也许想法可能来自军队?分组发票如下:
第 28 步兵师
--1st Brigade
---1st BN
----A Co
----B Co
---2nd BN
----A Co
----B Co
--第 2 旅
---第 1 BN
----A连
----B连
---第 2 BN
----A连
----B连 --第
3 旅
---第 1 BN
----A Co
----B Co
---2nd BN
----A Co
----B Co
http://boards.straightdope.com/sdmb/showthread.php?t=432978
组不必是顺序但组中的数字可以

更新

将上述视为按地点、时间、人员等区分的组。例如:使用卖家临时 ID 创建组,每 10 天更改一次或按办公室/商店更改。

还有一个想法,你可能会说有点奇怪,但是……当我想到它时,我越来越喜欢它。为什么不倒数这些发票呢?选择一个大数字并倒计时。向上计数时很容易跟踪项目数量,但向下计数?怎么会有人猜到起点在哪里?它也很容易实现。

于 2013-08-10T06:50:42.120 回答
2

如果订单放在收件箱中,直到每天早上一个人处理它们,看到那个人直到 16:00 才开始创建我的发票,这会让我觉得他很忙。拿到 9:01 的发票让我觉得我是今天唯一的客户。

但是,如果您在我下订单时生成 ID,则时间戳不会告诉我任何信息。

因此,我认为我实际上喜欢时间戳,假设两个客户同时需要创建 ID 的冲突很少见。

于 2013-08-16T23:37:27.237 回答
1

您可以从下面的代码中看到,我使用 newsequentialid() 生成一个序列号,然后将其转换为 [bigint]。因为这会产生 4294967296 的一致增量,所以我只需将该数字除以表上的 [id](它可以是 rand() 以纳秒或类似的方式播种)。结果是一个始终小于 4294967296 的数字,因此我可以安全地添加它并确保我不会与下一个数字的范围重叠。

和平凯瑟琳

declare @generator as table ( 
[id] [bigint], 
[guid] [uniqueidentifier] default( newsequentialid()) not null, 
[converted] as (convert([bigint], convert ([varbinary](8), [guid], 1))) + 10000000000000000000,
[converted_with_randomizer] as (convert([bigint], convert ([varbinary](8), [guid], 1))) + 10000000000000000000 + cast((4294967296 / [id]) as [bigint])
);

insert into @generator ([id])
values      (1), (2), (3), (4), (5), (6), (7), (8), (9), (10);

select [id],
   [guid],
   [converted],
   [converted] - lag([converted],
                     1.0)
                     over (
                         order by [id])                 as [orderly_increment],
   [converted_with_randomizer],
   [converted_with_randomizer] - lag([converted_with_randomizer],
                                     1.0)
                                     over (
                                         order by [id]) as [disorderly_increment]
from   @generator
order  by [converted]; 
于 2013-08-13T20:27:59.937 回答
1

我不知道您在发票 ID 上设置规则的原因,但您可以考虑拥有一个内部发票 ID,它可以是连续的 32 位整数和一个可以与客户共享的外部发票 ID。

这样,您的内部 ID 可以从 1 开始,您每次都可以添加一个,客户发票 ID 可以是您想要的。

于 2013-08-14T13:49:23.493 回答
0

我认为娜娜选择一个大数字并倒数的想法是正确的。从一个大值种子开始,向上或向下计数,但不要从最后一个占位符开始。如果您使用其他占位符之一,则会产生更高发票数量的错觉......如果他们实际上正在查看它。

这里唯一需要注意的是定期修改数字的最后 X 位以保持变化的外观。

于 2013-08-13T19:19:04.953 回答
0

为什么不采用一个易于阅读的数字,如

  • 前 12 位数字是 yyyymmddhhmm 格式的日期时间(确保发票 ID 的顺序)
  • 最后 x 位是订单号(在本例中为 8 位)

你得到的数字类似于 20130814140300000008

然后用它做一些简单的计算,比如前 12 位数字

(201308141403) * 3 = 603924424209

第二部分(原始:00000008)可以像这样混淆:

(10001234 - 00000008 * 256) * (minutes + 2) = 49995930  

很容易将其翻译回一个易于阅读的数字,但除非您不知道客户是如何完全不知道的。

对于 2013 年 8 月 14 日 14:03 的内部发票编号为 00000008 的发票,此编号总共看起来像603924424209-49995930 。

于 2013-08-14T12:21:43.430 回答
0

您可以编写自己的函数,将其应用于前一个数字时会生成下一个顺序随机数,该随机数大于前一个随机数。尽管可以生成的数字将来自有限集合(例如,1 到 2 次方 31 之间的整数),并且最终可能会重复,尽管可能性很小。要为生成的数字添加更多复杂性,您可以在末尾添加一些 AlphaNumeric 字符。您可以在此处阅读有关此内容的Sequential Random Numbers

一个示例生成器可以是

private static string GetNextnumber(int currentNumber)
{
      Int32 nextnumber = currentNumber + (currentNumber % 3) + 5;
      Random _random = new Random();
      //you can skip the below 2 lines if you don't want alpha numeric
      int num = _random.Next(0, 26); // Zero to 25
      char let = (char)('a' + num);
      return nextnumber + let.ToString();
}

你可以打电话

string nextnumber = GetNextnumber(yourpreviouslyGeneratedNumber);
于 2013-08-15T11:53:12.793 回答