Implementing Secure User Authentication in PHP Applications with Long-Term Persistence (Login with "Remember Me" Cookies)
A common problem in web development is to implement user authentication and access controls, typically accomplished through sign-up and log-in forms.Though these systems are simple enough in theory, engineering one that lives up to application security standards is a daunting undertaking.
web开发中的一个常见问题是实现用户身份验证和访问控制,通常通过注册和登录表单完成。尽管这些系统在理论上足够简单,但设计一个符合应用安全标准的系统却是一项艰巨的任务。
Without a great deal of care and sophistication, authentication systems can be as fragile as a cardboard lemonade stand in a category five hurricane. However, for everything that can go wrong, there is an effective (and often simple) way to achieve a higher level of security and resilience.
如果没有足够的小心和复杂度,身份认证系统可能像五级飓风中的纸板柠檬水架一样脆弱。然而,对于所有可能出错的情况,都有一种有效(通常也很简单)的方法来实现更高级别的安全性和弹性。
At a Glance
- Secure Password Storage in 2015
- Persistent Authentication ("Remember Me" Checkboxes with Long-Term Cookies) Done Right
- Account Recovery ("Forgot Your Password?")
If you're confused about any of the terms used on this page,please feel free to consult our guide to cryptography terms and concepts first.
Passwords: Hashes, Salts, and Policies(密码:哈希、盐和策略)
The year was 2004. Already, collisions in the MD5 hash function were being circulated, spelling near-certain doom for the future of this (and related) cryptographic hash functions. Five years earlier, Niels Provos presented bcrypt at USENIX 99. The RFC for PBKDF2 had already been published for four years.
那是2004年。MD5散列函数中的冲突已经在流传,这几乎注定了这个(和相关的)加密散列函数的未来。五年前,尼尔斯·普罗沃斯在USENIX 99大会上提出了bcrypt。PBKDF2的RFC已经发布四年了。
Would you believe that there are still web programmers that use fast cryptographic hash functions such as MD5 and SHA1 for password storage in 2015? It has been clear to security experts for a long time that this is a bad idea.
你会相信在2015年仍有Web程序员使用快速加密散列函数(如MD5和SHA1)来存储密码吗?很长时间以来,安全专家都清楚这是一个坏主意。
Acceptable Password Storage Systems(可接受的密码存储系统)
There are only four password hashing algorithms that are currently trusted by professional cryptographers and security researchers to protect users' passwords:
目前只有四种密码散列算法受到专业密码学家和安全研究人员的信任来保护用户的密码:
- Argon2 (winner of the Password Hashing Competition)
- bcrypt
- scrypt
- PBKDF2 (Password-Based Key Derivation Function #2)
For most PHP developers whom cannot install PECL packages in their production environments, scrypt is not an option. If you can use scrypt, please do.
对于大多数无法在生产环境中安装PECL软件包的PHP开发人员来说,scrypt不是一个选项。如果你能使用scrypt,请使用。
Given the choice between bcrypt and PBKDF2, developers should choose bcrypt. Furthermore, they should use the existing password_hash() and password_verify() API instead of writing their own crypt()-based implementation.
如果要在bcrypt和PBKDF2之间进行选择,开发人员应该选择bcrypt。 此外,他们应该使用现有的password_hash()和password_ify()API,而不是编写自己的基于crypt()的实现。
Developers should refrain from generating their own salts; let password_hash() take care of that instead.
开发人员应避免产生自己的盐,而是让password_hash()来处理这一点。
Developers in other languages should refer to our guide on how to safely store your users' passwords.
其他语言的开发者应该参考我们关于如何安全存储用户密码的指南
Limitations of bcrypt(Bcrypt 的局限性)
There are two caveats to bcrypt that every developer should be aware of: It truncates passwords to 72 characters and also on NUL bytes. (This assumes single-byte character encoding; multibyte characters will hit the limit sooner.) Many developers try to solve the 72 character limit issue by pre-hashing the user's password, which can trigger the second. A dangerous example follows:
对于bcrypt,每个开发人员都应该知道两个注意事项:它将密码截断为72个字符,并使用NUL字节。(这假定是单字节字符编码;多字节字符将更快达到极限。)许多开发人员试图通过预先散列用户密码来解决72个字符的限制问题,这可能会引发第二个问题。下面是一个危险的例子:
$stored = password_hash(hash('sha256', $_POST['password'], true), PASSWORD_DEFAULT);
// ...
if (password_verify(hash('sha256', $_POST['password'], true), $stored)) {
// Success :D
} else {
// Failure :(
}There is a nontrivial chance that one of the raw bytes in the hash will be 0x00. The sooner this byte appears in the string, the cost of finding a collision becomes exponentially cheaper.
哈希中的一个原始字节很可能是0x00。这个字节在字符串中出现得越早,发现冲突的代价就会以指数方式降低。
For example, both 1]W and @1$ produce a SHA-256 hash output that begins with ab00.
例如,1]W和@1$都会产生以ab00开头的SHA-256哈希输出。
The solution, therefore, would be to pass the raw SHA-256 hash outputs through base64_encode() before passing them to bcrypt:
因此,解决方案是在将原始SHA-256哈希输出传递给bcrypt之前,先通过base64_encode()传递它们:
$stored = password_hash(
base64_encode(
hash('sha256', $_POST['password'], true)
),
PASSWORD_DEFAULT
);
// ...
if (password_verify(
base64_encode(
hash('sha256', $_POST['password'], true)
),
$stored
)) {
// Success :D
} else {
// Failure :(
}The above example will not truncate at 72 characters and is fully binary-safe, so early null bytes will not lead to security weaknesses. The best of both worlds.
上面的示例不会截断为72个字符,并且是完全二进制安全的,因此早期的空字节不会导致安全漏洞。 两全其美。
Additionally, you may want to use SHA-384 instead of SHA-256, since SHA-256 is vulnerable to length-extension attacks and SHA-384 is not.
此外,您可能希望使用 SHA-384而不是 SHA-256,因为 SHA-256容易受到长度扩展攻击,而 SHA-384不会。
关于上面所讲的bcrypt的限制,在当Bcrypt与其他Hash函数同时使用时造成的安全问题中有更详细的描述。
评论已关闭