feat: register, log in and manage member accounts
This commit is contained in:
1 parent
4f056e91d4
commit
d8a79e8bc8
4 files changed
+474
-2
No files matched your search
@@ -0,0 +1,189 @@
|
||||
package identity
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/dihemoe/alisten/internal/app/port"
|
||||
"github.com/dihemoe/alisten/internal/domain/member"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrRegistrationClosed = errors.New("注册已关闭")
|
||||
ErrAlreadyLoggedIn = errors.New("已经登录了账号,先退出再登录其他账号")
|
||||
ErrInvalidCredentials = errors.New("用户名或密码错误")
|
||||
ErrWrongCurrentPassword = errors.New("当前密码错误")
|
||||
ErrNotRegistered = errors.New("还没有注册账号")
|
||||
ErrTooManyAttempts = errors.New("尝试次数太多,请稍后再试。")
|
||||
ErrAccountNotFound = errors.New("没有这个用户名的账号")
|
||||
)
|
||||
|
||||
// 注册、登录和修改密码都要做一次 argon2id 计算:额度既防止猜密码,也防止耗尽计算槽位。
|
||||
const (
|
||||
registerBurst = 5
|
||||
registerRefill = 10 * time.Minute
|
||||
loginIPBurst = 10
|
||||
loginIPRefill = time.Minute
|
||||
loginUserBurst = 5
|
||||
loginUserRefill = time.Minute
|
||||
passwordBurst = 5
|
||||
passwordRefill = time.Minute
|
||||
)
|
||||
|
||||
type Account struct {
|
||||
Username string
|
||||
}
|
||||
|
||||
type Accounts struct {
|
||||
members port.Members
|
||||
site port.Site
|
||||
quota port.AccountQuota
|
||||
}
|
||||
|
||||
func NewAccounts(members port.Members, site port.Site, quota port.AccountQuota) *Accounts {
|
||||
return &Accounts{members: members, site: site, quota: quota}
|
||||
}
|
||||
|
||||
// 游客没有账号,ok 为 false。
|
||||
func (a *Accounts) Account(ctx context.Context, memberID string) (account Account, ok bool, err error) {
|
||||
username, err := a.members.Username(ctx, memberID)
|
||||
if err != nil {
|
||||
return Account{}, false, err
|
||||
}
|
||||
return Account{Username: username}, username != "", nil
|
||||
}
|
||||
|
||||
// 给游客设置用户名和密码,成员和它的会话都不变。username 和 password 已经按协议校验过格式。
|
||||
func (a *Accounts) Register(ctx context.Context, memberID, clientIP, username, password string) (Account, error) {
|
||||
settings, err := a.site.Settings(ctx)
|
||||
if err != nil {
|
||||
return Account{}, err
|
||||
}
|
||||
if !settings.RegistrationOpen {
|
||||
return Account{}, ErrRegistrationClosed
|
||||
}
|
||||
if err := a.take(ctx, "register:ip:"+clientIP, registerBurst, registerRefill); err != nil {
|
||||
return Account{}, err
|
||||
}
|
||||
hash, err := HashPassword(ctx, password)
|
||||
if err != nil {
|
||||
return Account{}, err
|
||||
}
|
||||
if err := a.members.Register(ctx, memberID, username, hash); err != nil {
|
||||
return Account{}, err
|
||||
}
|
||||
return Account{Username: username}, nil
|
||||
}
|
||||
|
||||
// current 是发出请求的成员,currentToken 是它的成员 token。只有游客能登录:成功后签发账号的新会话,游客的这个会话随即失效。
|
||||
func (a *Accounts) Login(ctx context.Context, current member.Member, currentToken, clientIP, username, password string) (Session, Account, error) {
|
||||
if _, registered, err := a.Account(ctx, current.ID); err != nil {
|
||||
return Session{}, Account{}, err
|
||||
} else if registered {
|
||||
return Session{}, Account{}, ErrAlreadyLoggedIn
|
||||
}
|
||||
// 按 IP 限制一个来源的尝试次数,按用户名限制一个账号被尝试的次数:换着 IP 猜同一个账号也会受限。
|
||||
if err := a.take(ctx, "login:ip:"+clientIP, loginIPBurst, loginIPRefill); err != nil {
|
||||
return Session{}, Account{}, err
|
||||
}
|
||||
if err := a.take(ctx, "login:user:"+username, loginUserBurst, loginUserRefill); err != nil {
|
||||
return Session{}, Account{}, err
|
||||
}
|
||||
account, err := a.verify(ctx, username, password)
|
||||
if err != nil {
|
||||
return Session{}, Account{}, err
|
||||
}
|
||||
token, hash := newSecret()
|
||||
if err := a.members.ReplaceSession(ctx, hashSecret(currentToken), account.Member.ID, hash); err != nil {
|
||||
return Session{}, Account{}, err
|
||||
}
|
||||
return Session{MemberToken: token, Member: account.Member}, Account{Username: account.Username}, nil
|
||||
}
|
||||
|
||||
func (a *Accounts) verify(ctx context.Context, username, password string) (port.Account, error) {
|
||||
account, err := a.members.AccountByUsername(ctx, username)
|
||||
if errors.Is(err, port.ErrMemberNotFound) {
|
||||
return port.Account{}, ErrInvalidCredentials
|
||||
}
|
||||
if err != nil {
|
||||
return port.Account{}, err
|
||||
}
|
||||
ok, err := checkPassword(ctx, account.PasswordHash, password)
|
||||
if err != nil {
|
||||
return port.Account{}, err
|
||||
}
|
||||
if !ok {
|
||||
return port.Account{}, ErrInvalidCredentials
|
||||
}
|
||||
return account, nil
|
||||
}
|
||||
|
||||
func (a *Accounts) Logout(ctx context.Context, token string) error {
|
||||
return a.members.DeleteSession(ctx, hashSecret(token))
|
||||
}
|
||||
|
||||
func (a *Accounts) LogoutOthers(ctx context.Context, memberID, token string) error {
|
||||
return a.members.DeleteSessions(ctx, memberID, hashSecret(token))
|
||||
}
|
||||
|
||||
// 修改成功后,账号除了发出请求的这个会话,其他会话都失效。
|
||||
func (a *Accounts) ChangePassword(ctx context.Context, memberID, token, currentPassword, newPassword string) error {
|
||||
username, err := a.members.Username(ctx, memberID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if username == "" {
|
||||
return ErrNotRegistered
|
||||
}
|
||||
if err := a.take(ctx, "password:member:"+memberID, passwordBurst, passwordRefill); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := a.verify(ctx, username, currentPassword); errors.Is(err, ErrInvalidCredentials) {
|
||||
return ErrWrongCurrentPassword
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
hash, err := HashPassword(ctx, newPassword)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return a.members.SetPassword(ctx, memberID, hash, hashSecret(token))
|
||||
}
|
||||
|
||||
// Find 和 ResetPassword 只供后台使用,调用方要先校验管理 token。
|
||||
func (a *Accounts) Find(ctx context.Context, username string) (port.Account, error) {
|
||||
account, err := a.members.AccountByUsername(ctx, username)
|
||||
if errors.Is(err, port.ErrMemberNotFound) {
|
||||
return port.Account{}, ErrAccountNotFound
|
||||
}
|
||||
return account, err
|
||||
}
|
||||
|
||||
// 生成随机的新密码,账号在所有设备上的会话随即失效。新密码只出现在返回值中。
|
||||
func (a *Accounts) ResetPassword(ctx context.Context, username string) (string, error) {
|
||||
account, err := a.Find(ctx, username)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
password := newPassword()
|
||||
hash, err := HashPassword(ctx, password)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := a.members.SetPassword(ctx, account.Member.ID, hash, nil); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return password, nil
|
||||
}
|
||||
|
||||
func (a *Accounts) take(ctx context.Context, key string, burst int, refill time.Duration) error {
|
||||
wait, err := a.quota.Take(ctx, key, burst, refill)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if wait > 0 {
|
||||
return ErrTooManyAttempts
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
package identity
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"slices"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/dihemoe/alisten/internal/app/port"
|
||||
"github.com/dihemoe/alisten/internal/app/port/porttest"
|
||||
"github.com/dihemoe/alisten/internal/domain/member"
|
||||
)
|
||||
|
||||
const clientIP = "203.0.113.7"
|
||||
|
||||
type fakeQuota struct {
|
||||
mu sync.Mutex
|
||||
taken []string
|
||||
exhausted map[string]bool
|
||||
}
|
||||
|
||||
func (q *fakeQuota) Take(_ context.Context, key string, burst int, refill time.Duration) (time.Duration, error) {
|
||||
q.mu.Lock()
|
||||
defer q.mu.Unlock()
|
||||
if burst <= 0 || refill <= 0 {
|
||||
return 0, fmt.Errorf("额度参数不合理:burst=%d refill=%v", burst, refill)
|
||||
}
|
||||
q.taken = append(q.taken, key)
|
||||
if q.exhausted[key] {
|
||||
return time.Minute, nil
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func newAccounts(t *testing.T) (*Accounts, *Service, *porttest.Store, *fakeQuota) {
|
||||
t.Helper()
|
||||
s, store := newService(t)
|
||||
q := &fakeQuota{exhausted: map[string]bool{}}
|
||||
return NewAccounts(store, store, q), s, store, q
|
||||
}
|
||||
|
||||
// 返回 token 当前所属的成员;token 已经失效时 ok 为 false。
|
||||
func tokenOwner(t *testing.T, store *porttest.Store, token string) (m member.Member, ok bool) {
|
||||
t.Helper()
|
||||
m, err := store.MemberByToken(t.Context(), hashSecret(token))
|
||||
if errors.Is(err, port.ErrMemberNotFound) {
|
||||
return member.Member{}, false
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return m, true
|
||||
}
|
||||
|
||||
func registerAccount(t *testing.T, a *Accounts, s *Service, username, password string) Session {
|
||||
t.Helper()
|
||||
session := createMember(t, s)
|
||||
if _, err := a.Register(t.Context(), session.Member.ID, clientIP, username, password); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return session
|
||||
}
|
||||
|
||||
func TestRegister(t *testing.T) {
|
||||
a, s, store, q := newAccounts(t)
|
||||
guest := createMember(t, s)
|
||||
account, err := a.Register(t.Context(), guest.Member.ID, clientIP, "alice", "correct horse")
|
||||
if err != nil || account != (Account{Username: "alice"}) {
|
||||
t.Fatalf("Register = %#v, %v", account, err)
|
||||
}
|
||||
if got, ok, err := a.Account(t.Context(), guest.Member.ID); err != nil || !ok || got != account {
|
||||
t.Errorf("注册之后 Account = %#v, %v, %v", got, ok, err)
|
||||
}
|
||||
if m, ok := tokenOwner(t, store, guest.MemberToken); !ok || m != guest.Member {
|
||||
t.Error("注册不更换会话:原来的成员 token 应仍然有效")
|
||||
}
|
||||
stored, err := store.AccountByUsername(t.Context(), "alice")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if ok, err := checkPassword(t.Context(), stored.PasswordHash, "correct horse"); err != nil || !ok {
|
||||
t.Errorf("存储中应是密码的 argon2id 哈希:%v, %v", ok, err)
|
||||
}
|
||||
if want := []string{"register:ip:" + clientIP}; !slices.Equal(q.taken, want) {
|
||||
t.Errorf("取的额度 = %v,期望 %v", q.taken, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGuestHasNoAccount(t *testing.T) {
|
||||
a, s, _, _ := newAccounts(t)
|
||||
if _, ok, err := a.Account(t.Context(), createMember(t, s).Member.ID); err != nil || ok {
|
||||
t.Errorf("游客的 Account = %v, %v,期望没有账号", ok, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegisterRejected(t *testing.T) {
|
||||
a, s, store, q := newAccounts(t)
|
||||
alice := registerAccount(t, a, s, "alice", "correct horse")
|
||||
if _, err := a.Register(t.Context(), createMember(t, s).Member.ID, clientIP, "alice", "another horse"); !errors.Is(err, port.ErrUsernameTaken) {
|
||||
t.Errorf("用户名已被占用时 = %v,期望 ErrUsernameTaken", err)
|
||||
}
|
||||
if _, err := a.Register(t.Context(), alice.Member.ID, clientIP, "alice2", "another horse"); !errors.Is(err, port.ErrAlreadyRegistered) {
|
||||
t.Errorf("已经注册过时 = %v,期望 ErrAlreadyRegistered", err)
|
||||
}
|
||||
|
||||
guest := createMember(t, s)
|
||||
q.exhausted["register:ip:"+clientIP] = true
|
||||
if _, err := a.Register(t.Context(), guest.Member.ID, clientIP, "bob", "correct horse"); !errors.Is(err, ErrTooManyAttempts) {
|
||||
t.Errorf("额度用完时 = %v,期望 ErrTooManyAttempts", err)
|
||||
}
|
||||
if _, ok, _ := a.Account(t.Context(), guest.Member.ID); ok {
|
||||
t.Error("额度用完时不应注册")
|
||||
}
|
||||
|
||||
if err := store.SetRegistrationOpen(t.Context(), false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
q.taken = nil
|
||||
if _, err := a.Register(t.Context(), guest.Member.ID, "198.51.100.1", "bob", "correct horse"); !errors.Is(err, ErrRegistrationClosed) {
|
||||
t.Errorf("关闭注册时 = %v,期望 ErrRegistrationClosed", err)
|
||||
}
|
||||
if len(q.taken) != 0 {
|
||||
t.Errorf("关闭注册时不应消耗额度:%v", q.taken)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogin(t *testing.T) {
|
||||
a, s, store, q := newAccounts(t)
|
||||
alice := registerAccount(t, a, s, "alice", "correct horse")
|
||||
guest := createMember(t, s)
|
||||
q.taken = nil
|
||||
session, account, err := a.Login(t.Context(), guest.Member, guest.MemberToken, clientIP, "alice", "correct horse")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if session.Member != alice.Member || account != (Account{Username: "alice"}) {
|
||||
t.Errorf("Login = %#v, %#v,期望登录 alice", session.Member, account)
|
||||
}
|
||||
if session.MemberToken == alice.MemberToken || session.MemberToken == guest.MemberToken {
|
||||
t.Error("登录应签发新的成员 token")
|
||||
}
|
||||
if m, ok := tokenOwner(t, store, session.MemberToken); !ok || m != alice.Member {
|
||||
t.Errorf("新 token 属于 %#v,期望 alice", m)
|
||||
}
|
||||
if _, ok := tokenOwner(t, store, guest.MemberToken); ok {
|
||||
t.Error("登录之后游客原来的 token 应失效")
|
||||
}
|
||||
if _, ok := tokenOwner(t, store, alice.MemberToken); !ok {
|
||||
t.Error("登录不影响账号在其他设备上的会话")
|
||||
}
|
||||
if want := []string{"login:ip:" + clientIP, "login:user:alice"}; !slices.Equal(q.taken, want) {
|
||||
t.Errorf("取的额度 = %v,期望 %v", q.taken, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoginRejected(t *testing.T) {
|
||||
a, s, store, q := newAccounts(t)
|
||||
alice := registerAccount(t, a, s, "alice", "correct horse")
|
||||
guest := createMember(t, s)
|
||||
for _, tc := range []struct{ name, username, password string }{
|
||||
{"密码错误", "alice", "wrong horse"},
|
||||
{"没有这个用户名", "nobody", "correct horse"},
|
||||
} {
|
||||
if _, _, err := a.Login(t.Context(), guest.Member, guest.MemberToken, clientIP, tc.username, tc.password); !errors.Is(err, ErrInvalidCredentials) {
|
||||
t.Errorf("%s:Login = %v,期望 ErrInvalidCredentials", tc.name, err)
|
||||
}
|
||||
}
|
||||
if _, ok := tokenOwner(t, store, guest.MemberToken); !ok {
|
||||
t.Error("登录失败时游客的 token 应仍然有效")
|
||||
}
|
||||
if _, _, err := a.Login(t.Context(), alice.Member, alice.MemberToken, clientIP, "alice", "correct horse"); !errors.Is(err, ErrAlreadyLoggedIn) {
|
||||
t.Errorf("已注册的成员登录 = %v,期望 ErrAlreadyLoggedIn", err)
|
||||
}
|
||||
q.exhausted["login:user:alice"] = true
|
||||
if _, _, err := a.Login(t.Context(), guest.Member, guest.MemberToken, "198.51.100.1", "alice", "correct horse"); !errors.Is(err, ErrTooManyAttempts) {
|
||||
t.Errorf("用户名的额度用完时换一个 IP = %v,期望 ErrTooManyAttempts", err)
|
||||
}
|
||||
q.exhausted["login:ip:"+clientIP] = true
|
||||
if _, _, err := a.Login(t.Context(), guest.Member, guest.MemberToken, clientIP, "bob", "correct horse"); !errors.Is(err, ErrTooManyAttempts) {
|
||||
t.Errorf("IP 的额度用完时换一个用户名 = %v,期望 ErrTooManyAttempts", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogout(t *testing.T) {
|
||||
a, s, store, _ := newAccounts(t)
|
||||
alice := registerAccount(t, a, s, "alice", "correct horse")
|
||||
guest := createMember(t, s)
|
||||
other, _, err := a.Login(t.Context(), guest.Member, guest.MemberToken, clientIP, "alice", "correct horse")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := a.LogoutOthers(t.Context(), alice.Member.ID, alice.MemberToken); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, ok := tokenOwner(t, store, alice.MemberToken); !ok {
|
||||
t.Error("退出其他设备不影响发出请求的会话")
|
||||
}
|
||||
if _, ok := tokenOwner(t, store, other.MemberToken); ok {
|
||||
t.Error("退出其他设备之后其他会话应失效")
|
||||
}
|
||||
if err := a.Logout(t.Context(), alice.MemberToken); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, ok := tokenOwner(t, store, alice.MemberToken); ok {
|
||||
t.Error("退出登录之后 token 应失效")
|
||||
}
|
||||
}
|
||||
|
||||
func TestChangePassword(t *testing.T) {
|
||||
a, s, store, q := newAccounts(t)
|
||||
alice := registerAccount(t, a, s, "alice", "correct horse")
|
||||
guest := createMember(t, s)
|
||||
other, _, err := a.Login(t.Context(), guest.Member, guest.MemberToken, clientIP, "alice", "correct horse")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
q.taken = nil
|
||||
if err := a.ChangePassword(t.Context(), alice.Member.ID, alice.MemberToken, "wrong horse", "battery staple"); !errors.Is(err, ErrWrongCurrentPassword) {
|
||||
t.Errorf("当前密码错误时 = %v,期望 ErrWrongCurrentPassword", err)
|
||||
}
|
||||
if err := a.ChangePassword(t.Context(), alice.Member.ID, alice.MemberToken, "correct horse", "battery staple"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if want := []string{"password:member:" + alice.Member.ID, "password:member:" + alice.Member.ID}; !slices.Equal(q.taken, want) {
|
||||
t.Errorf("取的额度 = %v,期望 %v", q.taken, want)
|
||||
}
|
||||
if _, ok := tokenOwner(t, store, alice.MemberToken); !ok {
|
||||
t.Error("修改密码不影响发出请求的会话")
|
||||
}
|
||||
if _, ok := tokenOwner(t, store, other.MemberToken); ok {
|
||||
t.Error("修改密码之后其他会话应失效")
|
||||
}
|
||||
next := createMember(t, s)
|
||||
if _, _, err := a.Login(t.Context(), next.Member, next.MemberToken, clientIP, "alice", "correct horse"); !errors.Is(err, ErrInvalidCredentials) {
|
||||
t.Errorf("用旧密码登录 = %v,期望 ErrInvalidCredentials", err)
|
||||
}
|
||||
if _, _, err := a.Login(t.Context(), next.Member, next.MemberToken, clientIP, "alice", "battery staple"); err != nil {
|
||||
t.Errorf("用新密码登录 = %v", err)
|
||||
}
|
||||
stranger := createMember(t, s)
|
||||
if err := a.ChangePassword(t.Context(), stranger.Member.ID, stranger.MemberToken, "correct horse", "battery staple"); !errors.Is(err, ErrNotRegistered) {
|
||||
t.Errorf("游客修改密码 = %v,期望 ErrNotRegistered", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResetPassword(t *testing.T) {
|
||||
a, s, store, _ := newAccounts(t)
|
||||
alice := registerAccount(t, a, s, "alice", "correct horse")
|
||||
if _, err := a.Find(t.Context(), "nobody"); !errors.Is(err, ErrAccountNotFound) {
|
||||
t.Errorf("Find 不存在的用户名 = %v,期望 ErrAccountNotFound", err)
|
||||
}
|
||||
if _, err := a.ResetPassword(t.Context(), "nobody"); !errors.Is(err, ErrAccountNotFound) {
|
||||
t.Errorf("ResetPassword 不存在的用户名 = %v,期望 ErrAccountNotFound", err)
|
||||
}
|
||||
if found, err := a.Find(t.Context(), "alice"); err != nil || found.Member != alice.Member || found.Username != "alice" {
|
||||
t.Errorf("Find = %#v, %v", found, err)
|
||||
}
|
||||
password, err := a.ResetPassword(t.Context(), "alice")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !regexp.MustCompile(`^[A-Za-z0-9_-]{16}$`).MatchString(password) {
|
||||
t.Errorf("新密码 %q 应是 16 个 base64url 字符", password)
|
||||
}
|
||||
if _, ok := tokenOwner(t, store, alice.MemberToken); ok {
|
||||
t.Error("重置密码之后账号的所有会话应失效")
|
||||
}
|
||||
guest := createMember(t, s)
|
||||
if _, _, err := a.Login(t.Context(), guest.Member, guest.MemberToken, clientIP, "alice", password); err != nil {
|
||||
t.Errorf("用重置后的密码登录 = %v", err)
|
||||
}
|
||||
}
|
||||
@@ -24,9 +24,9 @@ const (
|
||||
|
||||
var passwordPrefix = fmt.Sprintf("$argon2id$v=%d$m=%d,t=%d,p=%d$", argon2.Version, argonMemory, argonTime, argonThreads)
|
||||
|
||||
var errMalformedPasswordHash = errors.New("房间密码哈希的格式不正确")
|
||||
var errMalformedPasswordHash = errors.New("密码哈希的格式不正确")
|
||||
|
||||
// 每次计算都要分配 argonMemory 的内存,而创建临时房间和换取票据任何人都能请求:不限制并发时,几十个请求就能耗尽内存。argon2 是纯计算,并发超过 GOMAXPROCS 也不会更快。
|
||||
// 每次计算都要分配 argonMemory 的内存,而创建临时房间、换取票据和登录任何人都能请求:不限制并发时,几十个请求就能耗尽内存。argon2 是纯计算,并发超过 GOMAXPROCS 也不会更快。
|
||||
var argonSlots = make(chan struct{}, runtime.GOMAXPROCS(0))
|
||||
|
||||
func idKey(ctx context.Context, password string, salt []byte) ([]byte, error) {
|
||||
|
||||
@@ -38,3 +38,10 @@ func avatarHash(email string) string {
|
||||
sum := md5.Sum([]byte(email))
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
// 后台重置密码时生成:96 位随机值的 base64url 编码,共 16 个字符,方便转告用户。
|
||||
func newPassword() string {
|
||||
b := make([]byte, 12)
|
||||
rand.Read(b)
|
||||
return base64.RawURLEncoding.EncodeToString(b)
|
||||
}
|
||||
Reference in new issue
Block a user