DB ID 需要一个较小的 GUID 替代方案,但 URL 仍然是唯一且随机的

Posted

技术标签:

【中文标题】DB ID 需要一个较小的 GUID 替代方案,但 URL 仍然是唯一且随机的【英文标题】:Need a smaller alternative to GUID for DB ID but still unique and random for URL 【发布时间】:2010-10-06 11:46:48 【问题描述】:

我已经为此找遍了所有地方,但似乎无法得到完整的答案。因此,如果 *** 上已经存在答案,那么我提前道歉。

我想要一个唯一且随机的 ID,这样我网站上的用户就无法猜测下一个数字,而只是跳转到其他人的信息。我计划坚持主键的递增 ID,但还要在数据库中为该行存储一个随机且唯一的 ID(某种哈希)并在其上放置一个索引。

从我的搜索中,我意识到我想避免冲突,并且我已经阅读了一些关于 SHA1 的提及。

我的基本要求是

小于 GUID 的东西。 (在 URL 中看起来很糟糕) 必须是唯一的 避免碰撞 不是一长串不可读的奇怪字符。

我正在寻找的一个例子是 www.somesite.com/page.aspx?id=AF78FEB

我不确定我应该在数据库中(我使用的是 SQL Server 2005)还是在代码中(我使用的是 C# ASP.Net)来实现它

编辑:

从我所做的所有阅读中,我意识到这是通过默默无闻的安全性。我确实打算对这些页面进行适当的授权和身份验证。我将使用 .Net 的身份验证和授权框架。但是,一旦合法用户登录并访问一个合法(但动态创建的页面),其中包含指向属于他的项目的链接。例如,链接可能是 www.site.com/page.aspx?item_id=123。是什么阻止他点击该链接,然后将上面的 URL 更改为不属于他的 www.site.com/page.aspx?item_id=456?我知道一些 Java 技术,如 Struts(我有待更正)将所有内容存储在会话中并以某种方式从中解决,但我不知道这是如何完成的。

【问题讨论】:

将此类数值转换为较短文本值的 URL 友好编码是 base62,它是字母数字。不幸的是,实现也很少见。做对是很棘手的。相反,您可以查看 base64-url,这是一种对 URL 友好的 base64 变体,比 base62 更常见。 【参考方案1】:

Raymond Chen 有一篇关于为什么不应该使用“半个 guid”的好文章,并在此处提供了一个合适的解决方案来生成您自己的“不太 guid 但足够好”的类型值:

GUIDs are globally unique, but substrings of GUIDs aren't

他的策略(没有具体实施)基于:

四位编码计算机编号, 时间戳为 56 位,并且 四位作为唯一符。

我们可以减少比特数以使计算机唯一,因为集群中的计算机数量是有界的,并且我们可以通过假设程序不会服务 200 年来减少时间戳中的比特数从现在开始。

您可以通过假设时钟不会偏离偏差超过一个小时(例如)并且时钟不会每小时重置超过 16 次,从而摆脱 4 位唯一性。

【讨论】:

基于此删除了我的答案 :) 我仍然认为正确保护页面然后使用递增 ID 是最好的选择。 谢谢,这是一篇关于 GUID 解剖学的优秀文章! (并不是因为这让 Jon Skeet 删除了他的答案;) 实际上,读取该链接并考虑到他在同一台机器上使用相同的算法,他可以轻松地将其从 16 个字节减少到 10 个字节并且仍然有剩余空间 (128 - 48 - 6 = 74) . Raymond 甚至建议修剪另外 10 个“uniquifier”位,将其减少到 8 个字节。 为什么没有这个徽章? ;) 同意正确保护页面然后使用递增 id 将是可行的方法 - 事实上有很好的性能。不在数据库中使用 GUID 或类似 GUID 作为 id 的原因,尤其是对于索引列【参考方案2】:

更新(2017 年 2 月 4 日):Walter Stabosz 在原始代码中发现了一个错误。经过调查,发现了更多的错误,但是,我自己对代码进行了广泛的测试和修改,原作者 (CraigTP) 现在已经修复了所有这些问题。我已经用正确的工作版本更新了这里的代码,你也可以download a Visual Studio 2015 solution here 包含“短代码”生成代码和一个相当全面的测试套件来证明正确性。

我过去使用的一个有趣的机制是在内部只使用递增整数/长整数,但将该整数“映射”到字母数字“代码”。

示例

Console.WriteLine($"1371 as a shortcode is: ShortCodes.LongToShortCode(1371)");
Console.WriteLine($"12345 as a shortcode is: ShortCodes.LongToShortCode(12345)");
Console.WriteLine($"7422822196733609484 as a shortcode is: ShortCodes.LongToShortCode(7422822196733609484)");

Console.WriteLine($"abc as a long is: ShortCodes.ShortCodeToLong("abc")");
Console.WriteLine($"ir6 as a long is: ShortCodes.ShortCodeToLong("ir6")");
Console.WriteLine($"atnhb4evqqcyx as a long is: ShortCodes.ShortCodeToLong("atnhb4evqqcyx")");    

// PLh7lX5fsEKqLgMrI9zCIA   
Console.WriteLine(GuidToShortGuid( Guid.Parse("957bb83c-5f7e-42b0-aa2e-032b23dcc220") ) );      

代码

以下代码显示了一个简单的类,它将把 long 更改为“代码”(然后又变回来!):

public static class ShortCodes

    // You may change the "shortcode_Keyspace" variable to contain as many or as few characters as you
    // please.  The more characters that are included in the "shortcode_Keyspace" constant, the shorter
    // the codes you can produce for a given long.
    private static string shortcodeKeyspace = "abcdefghijklmnopqrstuvwxyz0123456789";

    public static string LongToShortCode(long number)
    
        // Guard clause.  If passed 0 as input
        // we always return empty string.
        if (number == 0)
        
            return string.Empty;
        

        var keyspaceLength = shortcodeKeyspace.Length;
        var shortcodeResult = "";
        var numberToEncode = number;
        var i = 0;
        do
        
            i++;
            var characterValue = numberToEncode % keyspaceLength == 0 ? keyspaceLength : numberToEncode % keyspaceLength;
            var indexer = (int) characterValue - 1;
            shortcodeResult = shortcodeKeyspace[indexer] + shortcodeResult;
            numberToEncode = ((numberToEncode - characterValue) / keyspaceLength);
        
        while (numberToEncode != 0);
        return shortcodeResult;
    

    public static long ShortCodeToLong(string shortcode)
    
        var keyspaceLength = shortcodeKeyspace.Length;
        long shortcodeResult = 0;
        var shortcodeLength = shortcode.Length;
        var codeToDecode = shortcode;
        foreach (var character in codeToDecode)
        
            shortcodeLength--;
            var codeChar = character;
            var codeCharIndex = shortcodeKeyspace.IndexOf(codeChar);
            if (codeCharIndex < 0)
            
                // The character is not part of the keyspace and so entire shortcode is invalid.
                return 0;
            
            try
            
                checked
                
                    shortcodeResult += (codeCharIndex + 1) * (long) (Math.Pow(keyspaceLength, shortcodeLength));
                
            
            catch(OverflowException)
            
                // We've overflowed the maximum size for a long (possibly the shortcode is invalid or too long).
                return 0;
            
        
        return shortcodeResult;
    

这本质上是您自己的 baseX 编号系统(其中 X 是 shortCode_Keyspace 常量中唯一字符的数量。

为了使事情变得不可预测,请从 1 或 0 以外的其他值开始您的内部递增编号(即从 184723 开始),并更改 shortCode_Keyspace 常量中字符的顺序(即使用字母 AZ 和数字 0-9 , 但会在常量字符串中弄乱它们的顺序。这将有助于使每个代码有些不可预测。

如果你用它来“保护”任何东西,这仍然是隐蔽的安全性,如果给定的用户可以观察到足够多的这些生成的代码,他们就可以预测给定时间的相关代码。这样做的“安全性”(如果你可以这么说的话)是 shortCode_Keyspace 常量被加扰,并且保持秘密。

编辑: 如果您只想生成一个 GUID,并将其转换为仍然唯一但包含的字符较少的东西,那么这个小函数就可以解决问题:

public static string GuidToShortGuid(Guid gooid)

    string encoded = Convert.ToBase64String(gooid.ToByteArray());
    encoded = encoded.Replace("/", "_").Replace("+", "-");
    return encoded.Substring(0, 22);

【讨论】:

@CraidTP 我认为您的代码中存在错误。请参阅我添加到您的答案中的 Example 部分中的 cmets。 @WalterStabosz 你是对的。事实上,经过进一步调查,原始代码中还发现了许多其他错误。我已经完全重写了代码以修复错误并使用正确的工作版本更新了此处的代码。【参考方案3】:

如果您不希望其他用户看到人员信息,为什么不保护您正在使用 id 的页面?

如果你这样做了,那么如果你使用递增的 Id 就没有关系了。

【讨论】:

这些页面是安全的,但我需要一个属于该用户的项目列表才能显示在页面中。所以我不希望他们通过篡改 URL 来尝试查看不属于他们的项目。 如果页面是安全的,他们怎么能通过篡改看到不属于他们的项目? LongHorn 的意思是,如果它得到了适当的保护,他们猜到 URL 也没关系。 这是正确的答案。如果网站是安全的,你(提问者)为什么关心人们会做什么? 让我澄清一下,我不是在谈论猜测 URL。这些页面将受到保护,我将使用 .Nets 身份验证和授权。我说的是 www.site.com/page.aspx?item=123 是什么阻止他将网址更改为 www.site.com/page.aspx?item=456 而第 456 项不是他的。【参考方案4】:

[回应编辑] 您应该将查询字符串视为“恶意输入”。您需要以编程方式检查是否允许经过身份验证的用户查看请求的项目。

if( !item456.BelongsTo(user123) )

  // Either show them one of their items or a show an error message.

【讨论】:

我刚刚得出这个结论 :)【参考方案5】:

您可以随机生成一个数字。检查此号码是否已在数据库中并使用它。如果您希望它显示为随机字符串,您可以将其转换为十六进制,这样您就可以像示例中一样在其中获得 A-F。

【讨论】:

【参考方案6】:

GUID 是 128 位的。如果您采用这些位并且不使用只有 16 个字符的字符集来表示它们(16=2^4 和 128/4 = 32 个字符),而是使用 64 个字符的字符集(例如 Base 64) ,你最终只会有 22 个字符(64=2^6 和 128/6 = 21.333,所以 22 个字符)。

【讨论】:

【参考方案7】:

获取您的自动增量 ID,并使用只有您知道的秘密对其进行 HMAC-SHA1。这将生成一个看起来随机的 160 位,隐藏真正的增量 ID。然后,取一个长度的前缀,使您的应用程序不太可能发生冲突——比如 64 位,您可以将其编码为 8 个字符。使用它作为你的字符串。

HMAC 将保证没有人可以从显示的位映射回基础数字。通过散列自动增量 ID,您可以确定它是唯一的。因此,您的冲突风险来自 SHA1 中 64 位部分冲突的可能性。使用此方法,您可以通过预先生成此方法生成的所有随机字符串(例如,最多您期望的行数)并检查来预先确定是否会发生任何冲突。

当然,如果您愿意在数据库列上指定一个唯一条件,那么简单地生成一个完全随机的数字也可以。你只需要小心随机性的来源。

【讨论】:

【参考方案8】:

多长时间才算过长?您可以将 GUID 转换为 Base 64,这最终会使其更短。

【讨论】:

【参考方案9】:

当我想要你想要的东西时,你能做的就是我做的事情。

    创建您的 GUID。

    去掉破折号,得到一个 您想要多长时间的子字符串 身份证

    检查该 ID 的数据库,如果它 存在转到第 1 步。

    插入记录。

这是确保其隐蔽性和独特性的最简单方法。

【讨论】:

【参考方案10】:

我刚刚有了一个想法,我看到 Greg 也指出了它。我将用户存储在具有用户 ID 的会话中。当我创建查询时,我将使用该用户 ID 加入用户表,如果结果集为空,那么我们知道他正在破解 URL,我可以重定向到错误页面。

【讨论】:

【参考方案11】:

GUID 只是一个数字

最新一代的GUID(第4版)基本上是一个大随机数*

因为它是一个很大的随机数,所以发生碰撞的机会非常小。

您可以使用 GUID 获得的最大数字已经结束:

5,000,000,000,000,000,000,000,000,000,000,000,000

因此,如果您生成两个 GUID,则第二个 GUID 与第一个 GUID 相同的机会是:

1 in 5,000,000,000,000,000,000,000,000,000,000,000,000

如果您生成 1000 亿个 GUID。

您的 1000 亿分之一 GUID 与其他 99,999,999,999 个 GUID 发生冲突的可能性是:

1 in 50,000,000,000,000,000,000,000,000

为什么是 128 位?

一个原因是计算机喜欢使用 8 位的倍数。

8、16、32、64、128 等

另一个原因是提出 GUID 的人觉得 64 还不够,而 256 太多了。

您需要 128 位吗?

不,您需要多少位取决于您希望生成多少数字以及您希望它们不会发生冲突的程度。

64 位示例

那么您的第二个数字与第一个数字发生冲突的可能性是:

1 in 18,000,000,000,000,000,000 (64 bit)

代替:

1 in 5,000,000,000,000,000,000,000,000,000,000,000,000 (128 bit)

第 1000 亿个这个数字呢?

你的第 1000 亿个数字与其他 99,999,999,999 发生冲突的可能性是:

1 in 180,000,000 (64 bit)

代替:

1 in 50,000,000,000,000,000,000,000,000 (128 bit)

那么你应该使用 64 位吗?

取决于您是否生成 1000 亿个数字?即使你当时是 180,000,000 会让你不舒服吗?

更多关于 GUID 的详细信息

我说的是第 4 版。

版本 4 实际上并没有将所有 128 位用于随机数部分,它使用 122 位。其他 6 位用于表示是 GUID 标准的第 4 版。

此答案中的数字基于 122 位。

是的,因为它只是一个随机数,您可以从中获取所需的位数。 (只要确保您不采用永远不会更改的 6 个版本控制位中的任何一个 - 见上文)。

您可以使用与 GUID 相同的随机数生成器来代替从 GUID 中获取位。

可能使用了操作系统自带的随机数生成器。

【讨论】:

"如果您生成 1000 亿个 GUID。您的第 1000 亿个 GUID 与其他 99,999,999,999 个 GUID 发生冲突的可能性是 50,000,000,000,000,000,000,000,000 分之一" 这听起来不对...不是更像(非常大约)1,000,000,000,000,000 分之一?要获得您引用的碰撞机会,您只需要大约 500,000 个向导。 (en.wikipedia.org/wiki/Universally_unique_identifier#Collisions) 根据***文章中的公式,我明白你的意思。我无法确定我的逻辑在哪里出错。假设我让你猜骰子的叫法,你的机会是六分之一。如果我让你在掷骰子之前猜两个数字,你的机会是六分之二,可以减少到三分之一。你可以想到每一个指导您已经在具有更大骰子的游戏中进行了猜测。在这种情况下,要获得 50% 的机会,您需要所有可能数字的 50%,即 (2^122)/2。结果是 2.6e36,而文章说你在 2.7e18 达到 50% 的碰撞几率。我想我不明白。 掷骰子两次而不发生碰撞是五分之六的机会。首先,你掷骰子。然后你再次滚动,有 5/6 的机会不会发生碰撞。掷骰子三次而不发生碰撞将是 (5/6) * (4/6) = (20/36) 等等......最终有大约 1.5% 的机会能够掷骰子六次,得到六个唯一的数字。 我想我现在明白了,感谢您花时间解释,我会考虑重写我的答案,谢谢!

以上是关于DB ID 需要一个较小的 GUID 替代方案,但 URL 仍然是唯一且随机的的主要内容,如果未能解决你的问题,请参考以下文章

UIScrollView 中较小的 UIView 以停止触摸

考虑到可扩展性和友好 URL 的 GUID 替代方案

用较小的球体最佳地填充3D球体

带有 GUID Id 的简洁 CRUD

oracle DB 中的自动增量替代方案

EF 4.1 CodeFirst 与 Guid(不是由 DB/Store 生成)作为 PK