Skip to content

我会如何实现身份验证 #173

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 96 additions & 5 deletions _drafts/Article/Translation/how-i-would-do-auth.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,107 @@
---
title: How I would do auth
title: 我会如何实现身份验证
date: 2025-03-10T03:29:26.732Z
authorURL: ""
originalURL: https://pilcrowonpaper.com/blog/how-i-would-do-auth/
translator: ""
reviewer: ""
---

Copyright © pilcrowonpaper
这是一篇关于我如何为面向公众的应用实现身份验证的简短文章。这不会是太深入或权威的内容 - 只是我当前观点的集合。不过,对一些人来说,它可能作为入门指南很有用。

<!-- more -->
首先,如果应用是为开发者设计的,而我需要一些非常快速的东西,我会直接使用 GitHub OAuth。10 分钟内搞定。

[Source code][1]
现在进入主要部分 - 我会如何实现基于密码的身份验证?对我来说,最低要求是密码加上使用认证器应用的双因素认证(2FA)。密钥通行证(Passkeys)还不够普及,而我只是觉得魔术链接(magic-links)很烦人。

[1]: https://github.com/pilcrowonpaper/pilcrow
> 始终实现速率限制,即使是非常基础的实现!

会话管理
------------------

100%使用数据库会话。我真的非常不喜欢 JWT,大多数情况下它们不应该被用作会话。

假设我只需要处理已认证的会话,我首选的方法是 30 天过期,但每次使用会话时都会延长过期时间。这确保活跃用户保持认证状态,而不活跃用户则会被登出。

注册
------------

有争议的观点 - 我认为应用分享电子邮件是否已存在于系统中是可以的。如果电子邮件已被占用,只需告诉用户他们已经有一个账户。显著更好的用户体验,安全性损失很小。如果你不喜欢这样,就不要使用电子邮件进行身份验证。

无论如何,比防止用户枚举更重要的是检查密码是否在之前的泄露中出现过。[`haveibeenpwned.com`][1] API 可能是这方面的最佳选择。这将减少凭证填充攻击的有效性,即攻击者使用从其他网站泄露的密码来攻击账户。

密码使用 Argon2id 或 Scrypt 进行哈希处理 - 它们都足够好。Bcrypt 也可以,但不幸的是它有 50-70 个字符的限制。

速率限制将设置为每个 IP 地址每秒约 1 次尝试。如果开始收到垃圾信息,则使用验证码。

### 电子邮件验证

首先,我不会费心使用那些 100 个字符长的正则表达式。这是你唯一需要的电子邮件正则表达式:

```regex
^.+@.+\..+$
```

我还会检查电子邮件是否以空格开头或结尾,以确保用户没有输错。

对于电子邮件验证,我个人更喜欢一次性密码(OTP)而不是链接,但两者都可以。对于 OTP,每个账户每小时 5-10 次尝试的基本限制应该足够了。验证码将在 10 分钟,最多 15 分钟内有效。对于验证链接,我会将过期时间设置为 2 小时。

以下是我生成这些 OTP 的一些方法:

```golang
bytes := make([]byte, 5)
rand.Read(bytes)
// 8个字符,40位熵
// 我可能会使用自定义字符集来移除1、I、0和O。
otp := base32.StdEncoding.EncodeToString(bytes)
```

```golang
// 8个字符,熵相当于~26位
// 这引入了微小的偏差。
// 参见RFC 4226了解为什么这样做没问题。
bytes := make([]byte, 4)
rand.Read(bytes)
num := int(binary.BigEndian.Uint32(bytes) % 100000000)
otp := fmt.Sprintf("%08d", num)
```

登录
-----

同样,如果电子邮件无效,只需返回"账户不存在"。

登录限制将基于电子邮件。增加超时时间直到 5-10 分钟(例如 1、2、4、8、15、30、60、120、300 秒)。你不希望时间更长,以防止攻击者通过故意失败来阻止合法尝试。基于 IP 地址的速率限制将设置为每个 IP 每秒 1 次尝试。多个用户可以共享 IP 地址,所以我不想在这里太严格。无论如何,我对登录限制不太担心,因为我们在注册期间检查密码强度,并且我们启用了 2FA。

如果我真的担心账户锁定,我可能会考虑实现[设备 cookie][2],尽管如果我处理这类攻击,我可能需要监控请求并手动阻止请求。

2FA
---

对我来说,2FA 是必须的。应该始终提供通过认证器应用(TOTP)的 2FA。它对用户来说相对容易使用,对我来说也容易实现。密钥通行证和安全密钥将是我的下一个优先事项(注册密钥通行证的用户也应该被允许使用它们代替密码+2FA)。另一方面,SMS 成本高且容易受到 SIM 卡交换攻击。我会避免使用它。反正没人喜欢它们。

对于 TOTP,同样每个账户每小时 5-10 次尝试的基本限制应该足够了。对于密钥通行证,可能是每个 IP 地址每秒 1 次尝试。暴力破解密钥通行证是不可能的,但验证签名在某种程度上是资源密集型的,因此可能容易受到 DoS 攻击。

> 密钥通行证可以用作第二因素,也可以作为密码的替代品,但安全密钥应该只用作第二因素。

可选的,但如果我要实现恢复码,我会生成 5 或 10 个字节并进行 base32 编码,就像我在电子邮件验证部分展示的那样。使用 Argon2id/Scrypt 对它们进行哈希处理,并给用户随时重新生成它们的选项。它将是一次性使用的,使用它将断开与账户关联的所有 2FA 方法。我在这里会对尝试次数相当严格,设置为每个账户每小时或甚至每天 5 次尝试。

最后,用户应该使用他们的第二因素之一进行身份验证,然后才能编辑他们的 2FA 方法。这对用户来说会开始变得烦人,所以我只会每小时(或更少)要求一次,通过在会话中存储他们上次使用 2FA 的时间。

密码重置
--------------

同样,我不介意告诉用户电子邮件是否有效。

登录限制和速率限制将与登录非常相似,并将基于电子邮件和 IP 地址。必要时添加验证码。

一次性 OTP 和链接都可以工作,它们的过期时间将类似于电子邮件验证。我会对代码或令牌进行哈希处理以确保安全,特别是因为这并不难。

即使是密码重置,也应该需要 2FA。

我遗漏了什么吗?
--------------------

如果有任何我应该添加到文章中的内容,请在 Twitter 或 Discord 上告诉我!

[1]: https://haveibeenpwned.com/
[2]: https://owasp.org/www-community/Slow_Down_Online_Guessing_Attacks_with_Device_Cookies