如何生成一个随机、唯一、包含字母和数字的字符串?

493

如何使用数字和字母生成一个随机且唯一的字符串用于验证链接?就像在网站上创建账户并通过电子邮件发送带有链接的邮件,您需要点击该链接以验证您的账户一样。

我如何使用PHP生成此类字符串?


1
你所需要的仅是字符串和均匀分布的随机数。 - Artelius
12
嗨,安德鲁,你应该选择“Scott”作为正确答案。 Scott正在使用OpenSSL的加密安全伪随机数生成器(CSPRNG),它将基于您的平台选择最安全的熵源。 - rook
3
阅读此内容的任何人,如果在2015年之后,请务必注意:https://paragonie.com/blog/2015/07/how-safely-generate-random-strings-and-integers-in-php 大多数最佳答案或多或少存在缺陷... - rugk
OpenSSL和uniqid是不安全的。使用类似于Random::alphanumericString($length)或者Random::alphanumericHumanString($length)这样的东西。 - caw
31个回答

676
PHP 7 标准库提供了 random_bytes($length) 函数,用于生成具有密码学安全性的伪随机字节。
示例:
$bytes = random_bytes(20);
var_dump(bin2hex($bytes));

上面的例子将会输出类似于:
string(40) "5fe69c95ed70a9869d9f9af7d8400a6673bb9ce9"

更多信息:http://php.net/manual/zh/function.random-bytes.php PHP 5(已过时) 我刚刚在研究如何解决同样的问题,但我还希望我的函数能够创建一个可用于密码找回的令牌。这意味着我需要限制令牌被猜测的能力。因为uniqid是基于时间的,而根据php.net的说法“返回值与microtime()略有不同”,uniqid不符合要求。PHP建议使用openssl_random_pseudo_bytes()来生成具有密码学安全性的令牌。
一个快速、简短而直接的答案是:
bin2hex(openssl_random_pseudo_bytes($bytes))

这将生成一个长度为 $bytes * 2 的随机字母数字字符串。不幸的是,它只包含字母表 [a-f][0-9],但它能正常工作。
以下是我能够满足条件的最强功能(这是Erik答案的实现版本)。
function crypto_rand_secure($min, $max)
{
    $range = $max - $min;
    if ($range < 1) return $min; // not so random...
    $log = ceil(log($range, 2));
    $bytes = (int) ($log / 8) + 1; // length in bytes
    $bits = (int) $log + 1; // length in bits
    $filter = (int) (1 << $bits) - 1; // set all lower bits to 1
    do {
        $rnd = hexdec(bin2hex(openssl_random_pseudo_bytes($bytes)));
        $rnd = $rnd & $filter; // discard irrelevant bits
    } while ($rnd > $range);
    return $min + $rnd;
}

function getToken($length)
{
    $token = "";
    $codeAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
    $codeAlphabet.= "abcdefghijklmnopqrstuvwxyz";
    $codeAlphabet.= "0123456789";
    $max = strlen($codeAlphabet); // edited

    for ($i=0; $i < $length; $i++) {
        $token .= $codeAlphabet[crypto_rand_secure(0, $max-1)];
    }

    return $token;
}

crypto_rand_secure($min, $max) 可以作为 rand() 或者 mt_rand 的替代品。它使用 openssl_random_pseudo_bytes 来帮助生成一个介于 $min 和 $max 之间的随机数。

getToken($length) 创建一个用于令牌中的字母表,并生成一个长度为 $length 的字符串。

来源:https://www.php.net/manual/en/function.openssl-random-pseudo-bytes.php#104322


2
同意。明文密码存储太糟糕了!(我使用blowfish。)但是在令牌生成后,它允许持有者重置密码,因此在有效期内令牌等效于密码(并被视为密码)。 - Scott
30
嘿,我将你出色的代码与方便的Kohana Text::random()方法功能相结合,创建了这个要点:https://gist.github.com/raveren/5555297 - raveren
12
太好了!我用10个字符的长度进行了测试,生成了100000个标记(token),仍然没有重复的! - Sharpless512
5
我使用了这个过程,但发现速度非常慢。当长度为36时,在开发服务器上超时了。当长度为24时,仍需要大约30秒的时间。不确定自己哪里做错了,但对于我来说速度太慢了,所以我选择了另一个解决方案。 - Shane
6
你也可以使用base64_encode代替bin2hex来得到一个字母表更大、字符串更短的编码。 - erjiang
显示剩余26条评论

345

安全提示:本解决方案不应在随机性质量可能影响应用程序安全性的情况下使用。特别是,rand()uniqid() 不是加密安全随机数生成器。请参阅Scott的答案以获取安全替代方案。

如果您不需要它在时间上是绝对唯一的:

md5(uniqid(rand(), true))

否则(假设您已经确定了用户的唯一登录):

md5(uniqid($your_user_login, true))

101
两种方法都不能保证唯一性 - md5函数的输入长度大于其输出长度,根据http://en.wikipedia.org/wiki/Pigeonhole_principle,碰撞是可以被保证的。另一方面,依靠哈希实现“唯一”id的人口越多,至少发生一次碰撞的概率就越大(参见http://en.wikipedia.org/wiki/Birthday_problem)。对于大多数解决方案来说,这种概率可能很小,但它仍然存在。 - Dariusz Walczak
14
这不是生成随机值的安全方法。请查看Scott的回答。 - rook
7
对于那些很快就会过期的电子邮件确认,我认为某个人可能会有一天确认另一个人的电子邮件这种风险微乎其微。但是,在创建令牌后,您可以始终检查令牌是否已存在于数据库中,如果是,则选择一个新令牌。但是,您花费在编写该代码片段上的时间可能是浪费的,因为它很可能永远不会运行。 - Andrew
1
请注意,md5返回十六进制值,这意味着字符集仅限于[0-9]和[a-f]。 - Thijs Riezebeek
2
MD5 可能会发生碰撞,你刚刚破坏了 uniqid 的优势。 - user151496
str_shuffle(md5(uniqid(rand(), true))) - Davinder Kumar

104

最高赞解决方案的面向对象版本

我根据Scott的答案创建了一个面向对象的解决方案:

<?php

namespace Utils;

/**
 * Class RandomStringGenerator
 * @package Utils
 *
 * Solution taken from here:
 * https://dev59.com/1HI-5IYBdhLWcg3wc3-w#13733588
 */
class RandomStringGenerator
{
    /** @var string */
    protected $alphabet;

    /** @var int */
    protected $alphabetLength;


    /**
     * @param string $alphabet
     */
    public function __construct($alphabet = '')
    {
        if ('' !== $alphabet) {
            $this->setAlphabet($alphabet);
        } else {
            $this->setAlphabet(
                  implode(range('a', 'z'))
                . implode(range('A', 'Z'))
                . implode(range(0, 9))
            );
        }
    }

    /**
     * @param string $alphabet
     */
    public function setAlphabet($alphabet)
    {
        $this->alphabet = $alphabet;
        $this->alphabetLength = strlen($alphabet);
    }

    /**
     * @param int $length
     * @return string
     */
    public function generate($length)
    {
        $token = '';

        for ($i = 0; $i < $length; $i++) {
            $randomKey = $this->getRandomInteger(0, $this->alphabetLength);
            $token .= $this->alphabet[$randomKey];
        }

        return $token;
    }

    /**
     * @param int $min
     * @param int $max
     * @return int
     */
    protected function getRandomInteger($min, $max)
    {
        $range = ($max - $min);

        if ($range < 0) {
            // Not so random...
            return $min;
        }

        $log = log($range, 2);

        // Length in bytes.
        $bytes = (int) ($log / 8) + 1;

        // Length in bits.
        $bits = (int) $log + 1;

        // Set all lower bits to 1.
        $filter = (int) (1 << $bits) - 1;

        do {
            $rnd = hexdec(bin2hex(openssl_random_pseudo_bytes($bytes)));

            // Discard irrelevant bits.
            $rnd = $rnd & $filter;

        } while ($rnd >= $range);

        return ($min + $rnd);
    }
}

使用方法

<?php

use Utils\RandomStringGenerator;

// Create new instance of generator class.
$generator = new RandomStringGenerator;

// Set token length.
$tokenLength = 32;

// Call method to generate random string.
$token = $generator->generate($tokenLength);

自定义字母表

如果需要,您可以使用自定义字母表。 只需将包含支持字符的字符串传递到构造函数或设置器中即可:

<?php

$customAlphabet = '0123456789ABCDEF';

// Set initial alphabet.
$generator = new RandomStringGenerator($customAlphabet);

// Change alphabet whenever needed.
$generator->setAlphabet($customAlphabet);

这是输出样本

SRniGU2sRQb2K1ylXKnWwZr4HrtdRgrM
q1sRUjNq1K9rG905aneFzyD5IcqD4dlC
I0euIWffrURLKCCJZ5PQFcNUCto6cQfD
AKwPJMEM5ytgJyJyGqoD5FQwxv82YvMr
duoRF6gAawNOEQRICnOUNYmStWmOpEgS
sdHUkEn4565AJoTtkc8EqJ6cC4MLEHUx
eVywMdYXczuZmHaJ50nIVQjOidEVkVna
baJGt7cdLDbIxMctLsEBWgAw5BByP5V0
iqT0B2obq3oerbeXkDVLjZrrLheW4d8f
OUQYCny6tj2TYDlTuu1KsnUyaLkeObwa

希望这能帮助到别人。干杯!



对不起,我该怎么运行这个东西?我已经使用 $obj = new RandomStringGenerator; 了,但是应该调用哪个方法呢?谢谢。 - sg552
@sg552,你应该像上面的例子一样调用generate方法。只需传递一个整数作为参数,以指定生成字符串的长度。 - Slava Fomin II
谢谢,第一个例子对我有用,但是“自定义字母表”没有。当我回显时,输出为空。无论如何,我甚至不确定第二个例子“自定义字母表”的用途,所以我想我还是使用第一个例子。谢谢。 - sg552
1
不错,但在大多数情况下有点过度设计,除非你的项目严重依赖于随机生成的代码。 - Daniel
2
是的,很好,但有点过头了,特别是考虑到它现在在 PHP 5.7 中已经被淘汰了。 - Andrew

64

我根据Scott的回答提供的功能,提供了一些良好的研究数据。因此,我为这个5天长的自动化测试设置了一个Digital Ocean droplet,并将生成的唯一字符串存储在MySQL数据库中。

在这个测试期间,我使用了5个不同的长度(5、10、15、20、50),每个长度插入了+/-0.5百万条记录。在我的测试中,只有长度为5的字符串生成了+/-3K重复项(在50万项中),其余长度没有生成任何重复项。因此,我们可以说,如果我们使用长度为15或以上的Scott函数,则可以生成高度可靠的唯一字符串。下面是显示我的研究数据的表格:

输入图像描述

更新

我创建了一个简单的Heroku应用程序,使用这些函数作为JSON响应返回令牌。该应用程序可以通过https://uniquestrings.herokuapp.com/api/token?length=15访问。


为什么不简单地添加一个时间组件,比如microtime(),或者在生成时检查重复项呢?如果要求是“唯一”的,它就不能改变而不是唯一的。我很欣赏你的研究,但如果它不是绝对关键的话,它非常有用。 - ALZlper
@ALZlper microtime() 无法生成具有密码学安全性的字符串,如果涉及到安全问题,我们不能依赖它们。 - Rehmat
当然它不是加密安全的。这就是为什么我提到要在字符串中添加基于时间的部分。重要的是添加而不是替换你的答案。你想要更明确地表达这一点是正确的。我在另一个答案中也写了类似的东西。看看吧 :) https://dev59.com/JW0NtIcB2Jgan1znJW7s#70365951 - ALZlper

48
您可以使用 UUID(通用唯一标识符),它可用于任何目的,从用户身份验证字符串到支付交易 ID。
UUID 是一个16个八位字节(128位)的数字。在其规范形式中,UUID由32个十六进制数字表示,以五个由连字符分隔的组显示,形式为8-4-4-4-12,总共36个字符(32个字母数字字符和四个连字符)。
function generate_uuid() {
    return sprintf( '%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
        mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ),
        mt_rand( 0, 0xffff ),
        mt_rand( 0, 0x0C2f ) | 0x4000,
        mt_rand( 0, 0x3fff ) | 0x8000,
        mt_rand( 0, 0x2Aff ), mt_rand( 0, 0xffD3 ), mt_rand( 0, 0xff4B )
    );

}

//调用函数

$transationID = generate_uuid();

一些示例输出可能如下:

E302D66D-87E3-4450-8CB6-17531895BF14
22D288BC-7289-442B-BEEA-286777D559F2
51B4DE29-3B71-4FD2-9E6C-071703E1FF31
3777C8C6-9FF5-4C78-AAA2-08A47F555E81
54B91C72-2CF4-4501-A6E9-02A60DCBAE4C
60F75C7C-1AE3-417B-82C8-14D456542CD7
8DE0168D-01D3-4502-9E59-10D665CEBCB2

希望它能帮助未来的某个人 :)


看起来很棒,已加入我的列表! - Mustafa Erdogan

39

该函数将使用数字和字母生成随机密钥:

function random_string($length) {
    $key = '';
    $keys = array_merge(range(0, 9), range('a', 'z'));

    for ($i = 0; $i < $length; $i++) {
        $key .= $keys[array_rand($keys)];
    }

    return $key;
}

echo random_string(50);

示例输出:

zsd16xzv3jsytnp87tk7ygv73k8zmr0ekh6ly7mxaeyeh46oe8

6
一段时间后,你会看到相同的数字。 - Sharpless512
1
这不是完全随机的,因为它永远不会有相同的字符超过一次。 - EricP

18

我使用这个一行代码:

base64_encode(openssl_random_pseudo_bytes(3 * ($length >> 2)));

其中length是所需字符串的长度(必须为4的倍数,否则将被舍入为最接近的能被4整除的数字)


6
返回值不总是仅限于字母数字字符,它可能包含像 == 这样的字符。 - user3459110
1
如果在URL中使用,可以使用base64url_encode,请参见此处:http://php.net/manual/en/function.base64-encode.php - Paul

13

使用以下代码生成11个字符的随机数,或根据您的要求更改数字。

$randomNum=substr(str_shuffle("0123456789abcdefghijklmnopqrstvwxyz"), 0, 11);

或者我们可以使用自定义函数生成随机数。

 function randomNumber($length){
     $numbers = range(0,9);
     shuffle($numbers);
     for($i = 0;$i < $length;$i++)
        $digits .= $numbers[$i];
     return $digits;
 }

 //generate random number
 $randomNum=randomNumber(11);

4
这不能生成字符串"aaaaaaaaaaa"或"aaaaabbbbbb",它只是从字母表的随机排列中选择一个子串。这个长度为11个字符的字符串只有V(11,35) = 288种可能的取值。如果你希望字符串是唯一的,请不要使用它! - kdojeteri
1
@Peping 然后执行 substr(str_shuffle(str_repeat('0123456789abcdefghijklmnopqrstvwxyz', 36)), 0, 11); - Tim Hallman
1
感谢您提供的好函数!'0123456789abcdefghijklmnopqrstvwxyzABCDEFGHIJKLMNOPQRSTVWXYZ~!@#$%^&*()_+-=<>,.?/' - ext. str_shuffle 范围。 - vitali_y

8

对于真正随机的字符串,可以使用

<?php

echo md5(microtime(true).mt_Rand());

输出:

40a29479ec808ad4bcff288a48a25d5c

即使您在完全相同的时间尝试多次生成字符串,也会得到不同的输出。

8
  1. 使用您喜欢的随机数生成器生成一个随机数
  2. 将其乘以和除以字符表中字母数量相符的数字
  3. 获取字符表中该索引处的项
  4. 重复执行步骤1至3,直到获得所需长度

例如(伪代码)

int myInt = random(0, numcharacters)
char[] codealphabet = 'ABCDEF12345'
char random = codealphabet[i]
repeat until long enough

从理论上讲,这个过程有可能会生成相同的字符串超过一次,不是吗? - frezq

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接