如何在PostgreSQL中创建适用于会话ID的随机字符串?

133

我想在会话验证中使用PostgreSQL生成一个随机字符串。我知道可以使用SELECT random()获取随机数,所以我尝试了SELECT md5(random()),但那样不起作用。请问我该如何实现?


另一个解决方案可以在这里找到 http://stackoverflow.com/a/13675441/398670 - Craig Ringer
原始问题明确地提到了随机性具有超越外观的价值。我已经更新了问题的标题以反映@gersh的意图。 - Evan Carroll
8
我已经编辑了标题,以便现有的答案仍然非常合理,并且 Evan 的回答也很适合现代化。我不想因为内容争议而锁定这个古老的问题 - 所以让我们进行任何额外的编辑时,请考虑到_所有_的答案。 - Tim Post
1
很好,让我们看看@gersh能否澄清这个问题,因为对于他最初的意图存在合理的分歧。如果他最初的意图是我所认为的那样,那么这些答案中的许多需要进行调整、点踩或撤回。也许应该提出一个新问题,关于生成用于测试目的的字符串(或类似的内容),在这种情况下不需要“随机性”。如果不是我所认为的那样,那么我的答案就需要根据精细化的问题进行调整。 - Evan Carroll
5
@EvanCarroll - gersh 最后一次出现是在2015年11月21日。 - BSMP
5
如果有任何人在2017年之后来到这个问题,请考虑查看Evan的回答https://dev59.com/V2865IYBdhLWcg3wIa_M#41608000,因为他使用了在最初提问和回答时不可用的方法。请注意,这段话已经被翻译成中文。 - Marcin Raczkowski
13个回答

296
你可以像这样修复你的初步尝试:
SELECT md5(random()::text);

比其他一些建议简单得多。 :-)


19
请注意,此处仅返回“十六进制数字字母表”{0..9,a..f}中的字符串。可能不足够——取决于您想要对它们执行的操作。 - András Aszódi
返回的字符串长度是多少?有没有办法让它返回一个更长的字符串? - andrewrk
13
当用十六进制表示时,MD5字符串的长度始终为32个字符。如果您想要一个长度为64的字符串,可以将2个MD5字符串连接起来:`SELECT concat(md5(random()::text), md5(random()::text));`如果您想要中间某个位置的长度(例如50个字符),可以从其中取一个子字符串:`SELECT substr(concat(md5(random()::text), md5(random()::text)), 0, 50);` - Jimmie Tyrrell
4
会话ID的解决方案不是很好,随机性不太够。答案也已经有6年了。点击这里查看完全不同的方法,使用gen_random_uuid():更快、更具随机性,存储在数据库中更有效。 - Evan Carroll
@Evan 如果你想要更多的“随机性”而不需要扩展,你可以使用 SELECT md5(random()::text||random()::text); 或者 SELECT md5(random()::text||random()::text||random()::text); - user6854914
1
这是三倍的随机性,以及三倍的低效存储大小和md5开销。 - Evan Carroll

121
我建议使用这个简单的解决方案:
这是一个非常简单的函数,它返回给定长度的随机字符串:
Create or replace function random_string(length integer) returns text as
$$
declare
  chars text[] := '{0,1,2,3,4,5,6,7,8,9,A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z,a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z}';
  result text := '';
  i integer := 0;
begin
  if length < 0 then
    raise exception 'Given length cannot be less than 0';
  end if;
  for i in 1..length loop
    result := result || chars[1+random()*(array_length(chars, 1)-1)];
  end loop;
  return result;
end;
$$ language plpgsql;

使用方法:

select random_string(15);

示例输出:

select random_string(15) from generate_series(1,15);

  random_string
-----------------
 5emZKMYUB9C2vT6
 3i4JfnKraWduR0J
 R5xEfIZEllNynJR
 tMAxfql0iMWMIxM
 aPSYd7pDLcyibl2
 3fPDd54P5llb84Z
 VeywDb53oQfn9GZ
 BJGaXtfaIkN4NV8
 w1mvxzX33NTiBby
 knI1Opt4QDonHCJ
 P9KC5IBcLE0owBQ
 vvEEwc4qfV4VJLg
 ckpwwuG8YbMYQJi
 rFf6TchXTO3XsLs
 axdQvaLBitm6SDP
(15 rows)

7
这个解决方案使用了字符数组的两端值——0和z——比其他值少一半。为了让字符更均匀地分布,我用chars[ceil(61*random())]替换了chars[1+random()*(array_length(chars, 1)-1)] - PreciousBodilyFluids
random()被调用了length次(就像许多其他解决方案一样)。有没有更有效的方法每次从62个字符中选择?与md5()相比,它的性能如何? - ma11hew28
1
我发现了另一种解决方案,它使用ORDER BY random()。哪个更快? - ma11hew28
1
值得注意的是,random可能使用erand48,它不是CSPRNG,你最好使用pgcrypto。 - Yaur
2
除了没有使用安全的随机数生成器以外,这是一个很好的答案,因此不适合用于会话ID。请参见:https://dev59.com/ymHVa4cB1Zd3GeqPqdLQ - sudo
显示剩余2条评论

39
你可以从UUID中获取128位的随机数。这是在现代PostgreSQL中完成此工作的方法。
CREATE EXTENSION pgcrypto;
SELECT gen_random_uuid();

           gen_random_uuid            
--------------------------------------
 202ed325-b8b1-477f-8494-02475973a28f

也许 ��得阅读UUID文档

数据类型uuid将通用唯一标识符(UUID)存储为由RFC 4122、ISO / IEC 9834-8:2005和相关标准定义的128位数量。(某些系统称此数据类型为全局唯一标识符或GUID。)此标识符是通过选择的算法生成的,使得使用相同算法的其他人很难生成相同的标识符。因此,对于分布式系统,这些标识符提供比序列发生器更好的唯一性保证,后者仅在单个数据库中唯一。

假设UUID是随机的,它们的碰撞或可猜测性有多罕见?

生成约100万亿个版本4 UUID,才有1/10亿的几率出现重复。只有在生成261个UUID(2.3 x 10^18或2.3百万亿)后,重复的概率才会上升到50%。将这些数字与数据库相关联,并考虑Version 4 UUID冲突的概率是否可以忽略不计,假设一个包含2.3百万亿个Version 4 UUID的文件中,有50%的可能性包含一个UUID冲突。如果没有其他数据或开销,该文件的大小将达到36 exabytes,是当前存在的最大数据库的数千倍,它们的数量级为petabytes。以每秒1亿个UUID的速率生成该文件的UUID需要73年时间。假设没有备份或冗余,存储它需要约360万个10 TB硬盘或磁带盒。使用典型的“磁盘到缓冲区”传输速率为每秒1 Gbit的速率读取文件,单个处理器需要超过3000年的时间。由于驱动器的不可恢复读取错误率为每1018位读取1位,而该文件将包含约1020位,因此仅从头到尾一次读取文件就会导致至少比重复多约100倍的错误UUID。存储、网络、电源和其他硬件和软件错误无疑会比UUID重复问题频繁数千倍。

来源:维基百科

总之,

  • UUID是标准化的。
  • gen_random_uuid()是128位随机数,存储在128位中(2**128种组合)。零浪费。
  • random()只能在PostgreSQL中生成52位随机数(2**52种组合)。
  • md5()作为UUID存储是128位,但它只能像其输入一样随机(如果使用random(),则为52位
  • md5()作为文本存储是288位,但它只能像其输入一样随机(如果使用random(),则为52位)- 是UUID大小的两倍多,但随机性只有一小部分)
  • md5()作为哈希可以被优化得不会有效地做太多事情。
  • UUID对于存储非常高效:PostgreSQL提供了一个恰好为128位的类型。与存储为varlenatextvarchar等不同,后者需要存储字符串的长度开销。
  • PostgreSQL巧妙的UUID附带了一些默认的运算符、转换和特性。

5
部分不正确:一个正确生成的随机UUID只有122个随机比特,因为4个比特用于版本号,2个比特用于变体:https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_4_.28random.29 - Olivier Grégoire
2
如果源代码没有按照那里所写的去执行,那么它就不是UUID,PostgreSQL也不应该这样称呼它。 - Olivier Grégoire
1
请注意,修剪+编码(例如base64)会消除熵,并可能导致冲突。 - Antwan
1
@Antwan base64,作为一种可逆编码,不会移除任何熵。 - jbg
@jbg 剪裁操作 - Antwan
感谢您澄清MD5解决方案(52位)中随机性的限制! - sunsetjunks

36

在Marcin的解决方案基础上,你可以这样做来使用任意字母表(在这种情况下,所有 62 个 ASCII 字母数字字符):

SELECT array_to_string(array 
       ( 
              select substr('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', trunc(random() * 62)::integer + 1, 1)
              FROM   generate_series(1, 12)), '');

慢,不够随机,存储效率也不高。对于会话 ID 来说并不是一个很好的解决方案,随机性不足。而且这个答案已经有 6 年了。请查看此链接以获取完全不同的方法,使用 gen_random_uuid():速度更快,更随机,更有效地存储在数据库中。 - Evan Carroll

23

请使用 string_agg

SELECT string_agg (substr('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', ceil (random() * 62)::integer, 1), '')
FROM   generate_series(1, 45);

我也使用这个与MD5一起生成UUID。我只需要一个比random()整数更多位的随机值。


我想我可以通过连接 random() 直到得到所需的位数。哦,算了。 - Andrew Wolfe

19

最近我在使用PostgreSQL,我发现了一种更好的解决方案,只使用内置的PostgreSQL方法 - 不需要pl/pgsql。唯一的限制是它目前只能生成大写字符串、数字或小写字符串。

template1=> SELECT array_to_string(ARRAY(SELECT chr((65 + round(random() * 25)) :: integer) FROM generate_series(1,12)), '');
 array_to_string
-----------------
 TFBEGODDVTDM

template1=> SELECT array_to_string(ARRAY(SELECT chr((48 + round(random() * 9)) :: integer) FROM generate_series(1,12)), '');
 array_to_string
-----------------
 868778103681
< p> generate_series方法的第二个参数决定了字符串的长度。


9
我喜欢这个东西,但当我将其用作UPDATE语句时,所有行都被设置为相同的随机密码,而不是唯一的密码。我通过将主键ID添加到公式中来解决这个问题。我将它添加到随机值中,然后再减去它。随机性没有改变,但PostgreSQL被欺骗重新计算每行的值。这是一个示例,使用主键名称“my_id”:array_to_string(ARRAY(SELECT chr((65 + round((random()+my_id-my) * 25)) :: integer) FROM generate_series(1,8)), '') - Mark Stosberg
@MarkStosberg 提供的解决方案按他所说的起作用了,但不符合我的预期。生成的数据与预期的模式不匹配(仅包含字母或仅包含数字)。我通过对随机结果进行算术取模来修复:array_to_string(ARRAY(SELECT chr((65 + round((random() * 25 + id)::integer % 25))::integer) FROM generate_series(1,60)), ''); - Nuno Rafael Figueiredo
4
不行。你回答的是“我如何生成随机会话ID”,而不是“我如何生成随机字符串”。你根据描述中的两个单词改变了问题(和标题)的含义。你回答的是不同的问题,并滥用你的管理员权限来改变问题的意思。 - Marcin Raczkowski

11

虽然它默认不激活,但您可以激活其中一个核心扩展:

CREATE EXTENSION IF NOT EXISTS pgcrypto;

那么你的语句就变成了对gen_salt()函数的简单调用,该函数会生成一个随机字符串:

select gen_salt('md5') from generate_series(1,4);

 gen_salt
-----------
$1$M.QRlF4U
$1$cv7bNJDM
$1$av34779p
$1$ZQkrCXHD

首位数字是哈希标识符,有多种算法可用,每种都有自己的标识符:

  • md5:$1$
  • bf:$2a$06$
  • des:没有标识符
  • xdes:_J9..

有关扩展的更多信息:


编辑

正如Evan Carrol所指出的那样,从v9.4开始,您可以使用gen_random_uuid()

http://www.postgresql.org/docs/9.4/static/pgcrypto.html


生成的盐看起来太顺序了,不太可能是真正随机的,不是吗? - Le Droid
1
你是在指 $1$ 吗?那是一个哈希类型的标识符(md5==1),其余部分是随机值。 - Jefferey Cave
是的,那是我的错误解释,感谢您的精确说明。 - Le Droid

10

@Kavius建议使用pgcrypto,但是除了gen_salt之外,gen_random_bytes怎么样?还有sha512代替md5呢?

create extension if not exists pgcrypto;
select digest(gen_random_bytes(1024), 'sha512');

文档:

F.25.5. 随机数据函数

gen_random_bytes(count integer) 返回 bytea 类型

返回 count 个加密强度的随机字节。一次最多可以提取1024个字节。这是为了避免耗尽随机数生成器池。


我想知道如果你要哈希的底层数据是随机的,那么sha512是否有任何好处。假设数据是随机的,任何编码为字符串的东西都应该足够了,而且计算复杂度越低越好(例如base64编码?)——抱歉,这是一个旧评论,但在与同事讨论时又被提起。 - Jefferey Cave
@JeffereyCave,已经过去6年了,所以我不记得上下文是什么了,但看起来OP当时在询问会话ID,而sha512的优势在于比md5长4倍,因此碰撞攻击会更加困难? - Jared Beck
我同意越长越好的说法... 我本来以为你的回答在 select gen_random_bytes(1024) 就结束了.. 是啊... 6年了。和同事随便聊天时提起的。 - Jefferey Cave

8

INTEGER参数定义了字符串的长度。该方法保证所有62个字母和数字的概率相等,而不像互联网上其他解决方案那样存在偏差。

CREATE OR REPLACE FUNCTION random_string(INTEGER)
RETURNS TEXT AS
$BODY$
SELECT array_to_string(
    ARRAY (
        SELECT substring(
            '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
            FROM (ceil(random()*62))::int FOR 1
        )
        FROM generate_series(1, $1)
    ), 
    ''
)
$BODY$
LANGUAGE sql VOLATILE;

慢,不够随机,存储效率也不高。对于会话 ID 来说并不是一个很好的解决方案,随机性不足。而且这个答案已经有 6 年了。请查看此链接以获取完全不同的方法,使用 gen_random_uuid():速度更快,更随机,存储在数据库中更有效。 - Evan Carroll
3
公平地说,据我所知,gen_random_uuid() 函数似乎是在版本 9.4 中出现的,该版本发布于2014年12月18日,比你所反对的回答晚了一年多。另外一个小问题是:那个回答仅有3年半的历史 :-) 但你是对的,既然有了 gen_random_uuid(),就应该使用它。因此,我会点赞你的回答。 - András Aszódi

6
create extension if not exists pgcrypto;

那么。
SELECT encode(gen_random_bytes(20),'base64')

甚至更多

SELECT encode(gen_random_bytes(20),'hex')

这是用于 20 字节 = 160 位的随机数(例如sha1)。


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