不要使用UUID,它不安全!


如果您需要一个不可猜测的随机字符串(例如,用于会话 cookie 或访问令牌),可能很想获取一个随机 UUID,如下所示:

88cf3e49-e28e-4c0e-b95f-6a68a785a89d

这是一个 128 位值,格式为 36 个十六进制数字,用连字符分隔。在 Java 和大多数其他编程语言中,这些很容易生成:

import java.util.UUID;
String id = UUID.randomUUID().toString();

在引擎盖下,这使用了一个加密安全的伪随机数发生器(CSPRNG),所以生成的ID是相当独特的。然而,使用随机UUID有一些缺点,使它们没有最初看起来那么有用。在这篇笔记中,我将描述这些缺点以及我建议使用的替代方法。

...
详细点击标题

122 位 UUID 面对黑客攻击只能坚持……2048 秒,或者少于 35 分钟。

对 Java 的随机 UUID 实现的一个具体批评是:
它在内部使用单个共享SecureRandom实例来生成随机位。根据配置的后端,这可能会获得一个锁,如果您生成大量随机令牌,特别是如果您使用系统阻塞熵源(不要那样做,请使用 /dev/urandom),这可能会变得激烈竞争。通过滚动您自己的令牌生成,您可以使用本地线程或 SecureRandom 实例池来避免此类争用。(注意——NativePRNG 在内部使用共享静态锁,所以这在这种情况下没有帮助,但它也持有较短的关键部分的锁,因此不太容易出现问题)。

我们应该用什么代替?
我的建议是使用一个 160 位(20 字节)的随机值,然后对其进行URL 安全的 base64编码。URL 安全的 base64 变体几乎可以在任何地方使用,而且相当紧凑。在Java中:

import java.security.SecureRandom;
import java.util.Base64;

public class SecureRandomString {
    private static final SecureRandom random = new SecureRandom();
    private static final Base64.Encoder encoder = Base64.getUrlEncoder().withoutPadding();

    public static String generate() {
        byte[] buffer = new byte[20];
        random.nextBytes(buffer);
        return encoder.encodeToString(buffer);
    }
}

这产生的输出值如下:
Xl3S2itovd5CDS7cKSNvml4_ODA

这比 UUID 更短,而且具有 160 位的熵更安全。
如果需要,我还可以将 SecureRandom 变成 ThreadLocal。

那么我们的极端攻击者需要多长时间才能找到一个 160 位的随机令牌?大约一千七百九十万年。通过稍微调整我们的令牌格式,我们可以从担心攻击者的能力和资源转移到内心的平静和幸福。这远远超出了我们可以停止担心的可能范围。
为什么不更进一步呢?为什么不是 256 位?
这会将攻击成本推向更加荒谬的境地。我发现 160 位是出色安全性的最佳选择,同时仍然具有紧凑的字符串表示形式。