电子邮件确认码的最佳实践

15

我正在创建一个涉及用户注册的PHP网站,想知道“电子邮件确认”代码的最佳实践。

新用户必须确认他们的电子邮件地址 - 我通过生成一个代码并将其发送到用户的电子邮件中来完成这个过程,用户可以使用该代码激活他的账户。我没有将该代码存储在数据库中,而是使用了一个方便的小技巧:该代码是以下操作的结果:

md5("xxxxxxxxx".$username."xxxxxxxxxxx".$timestamp."xxxxxxxxx");

$timestamp指的是用户创建时间。总体而言,我对此感到相当满意,但是后来我开始想,这样安全吗?还有碰撞的可能性吗?我还需要为密码重置等生成代码。如果我使用类似的方法,碰撞可能会导致一个用户无意中重置另一个用户的密码。那可不好。

那么你该如何做呢?我的想法是创建一个格式如下的表:

codePK (int, a-I), userID (int), type (int), code (varchar 32), date (timestamp)

'type'的值可以是1、2或3,分别代表“激活”、“更改电子邮件”或“重置密码”。这是一个好的方法吗?您有更好的方法吗?

使用类似上述方法,我是否可以在不使用cron作业的情况下自动删除两天前的任何内容?我的主机(nearlyfreespeech.net)不支持它们。如果可能的话,我希望避免在外部主机上有一个wget脚本来删除东西,因为那太混乱了=P。

谢谢!
Mala

更新:
澄清一下:我意识到完成此任务的唯一安全可靠的方法是使用数据库,这也是原始函数试图避免使用的。我的问题是关于表(或表格)应该如何结构化。有人建议我放弃codePK,只使用code作为PK。简而言之,我的问题是:您是这样做的吗?


1
重要的是用户在档案中有一个有效的电子邮件地址,即使只是用于密码重置。虽然有人可能会说“那是用户的责任”,但我们正在处理现实世界...一旦第一个声称“永远不会忘记密码”的人忘记了密码,可能是一两周后,我的系统就会成为“最愚蠢的系统”。 - Mala
5个回答

10

当我需要这些技巧时,通常是出于两个原因之一,您都提到了:

  1. 用于验证发送给用户的电子邮件的密钥
  2. 用作密码重置链接的密钥

当然,在许多其他情况下,您可能会考虑使用这种构造。

首先,您应该始终使用某种隐藏且只有您知道的盐。请注意,此盐应对每个用户都不同。例如,可以将盐计算为 sha256(随机内容)。然后,应将此盐与用户名和密码(使用盐进行哈希)一起存储在数据库中。

当发送密码重置链接时,我会创建另一个盐(不要让用户访问任何使用您的盐进行哈希的内容。他知道自己的密码,因此使用暴力破解他可能会弄清楚您的盐)。 然后,将另一个盐(实质上仅为随机字符串的哈希值,您可能希望在此处使用md5,因为长度是一个问题),保存到数据库中。

通常,您只需向用户表添加一个附加列即可。然而,这也有一些问题,主要是一旦密码已重置或用户已激活,您就会从数据库中删除密钥,这会导致大多数行具有空值,这又会带来其他麻烦。

这实际上归结为以下几点:

  • 使用唯一的盐为用户的密码进行哈希处理(并且可能是全局的、保密的盐)。
  • 通过哈希多个随机或伪随机源(如时间戳、mt_rand()甚至random.org之类的东西)生成一个密钥。
  • 绝不要将全局盐或仅为用户唯一的盐用于对用户可以访问的任何内容进行哈希处理,包括密码重置密钥、激活密钥等。

请注意,我绝不是安全专家,我可能忘记了一些事情,还可能提到了一些非常糟糕的做法。只是我的想法;-)


2
为什么要使用用户数据作为授权密钥的基础?
我猜您正在数据库中存储已停用的数据,那么为什么不添加一个额外的记录,它只是一个随机密钥(可能是一个md5加密的uniqid并进行一些附加操作),然后对其进行检查呢?

最初我想避免使用数据库,因此才使用了userdata。 - Mala
1
说实话,我不禁想到数据库是最好的选择,否则你将会对一组非常有限的静态数据进行哈希/操作。 - John Parker

1

在你把你的方法发布到互联网之前,它是足够安全的!这是因为你依赖于安全性保密性,这不是一个好主意。

最好使用某种有键哈希函数或MAC,它包含仅你知道的秘密密钥。


在这种情况下,您已经创建了一个键控哈希函数,可以继续进行。我可能应该猜到了... ;) - David M
抱歉,我以为我已经指定了 - 但显然我编辑了那部分内容,在任何情况下,我想知道表结构/是否适合使用单个表来存储所有代码。 - Mala

0
为什么不将代码字段设置为唯一索引?这样就永远不会发生冲突了吧?
此外,如果您不需要从用户输入创建哈希并将其与数据库哈希匹配(例如电子邮件确认、密码重置等),您可以向哈希主体添加随机字符串,例如 md5('xxx'.$username.'xxx'.time().'xxx'.rand())

那样的话,我将无法在不使用数据库的情况下进行检查-该功能的整个目的就是避免使用它们。如果我仍然要使用数据库,那么我可以让整个字符串完全随机。 - Mala

0
为什么不让用户输入他们的用户名代码,从而消除任何碰撞问题?在安全方面,您不会失去任何东西,因为您仍然要求他们提供从电子邮件中获取的密钥,但您将阻止他们能够重置其他用户的密码。

1
理想情况下,他们只需在电子邮件中点击链接即可。我不希望这太长了 =P - Mala

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