feat: 补齐模块接口契约与管理 API 文档
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
// Package auth 定义密码哈希池、会话令牌、API 令牌与登录锁定计数的接口。
|
||||
// 本包在 T0.4 只提供契约与测试假实现;真正的 argon2 池与令牌逻辑由平台 P 实现。
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ErrNotImplemented 表示假实现尚未提供业务能力。
|
||||
var ErrNotImplemented = errors.New("auth: not implemented")
|
||||
|
||||
// PasswordKind 区分哈希用途(便于日志与限流,不影响算法)。
|
||||
type PasswordKind string
|
||||
|
||||
const (
|
||||
PasswordLogin PasswordKind = "login"
|
||||
PasswordTalk PasswordKind = "talk"
|
||||
PasswordAdmin PasswordKind = "admin"
|
||||
)
|
||||
|
||||
// HashPool 是 argon2id 并发池(DEVELOPMENT 第 12 节)。
|
||||
type HashPool interface {
|
||||
// Hash 计算 PHC 格式哈希。
|
||||
Hash(ctx context.Context, kind PasswordKind, password string) (phc string, err error)
|
||||
// Verify 常量时间比较;ok 为 false 时 err 仍可为 nil(密码不匹配)。
|
||||
Verify(ctx context.Context, kind PasswordKind, password, phc string) (ok bool, err error)
|
||||
// QueueLen 返回等待哈希的任务数(指标用)。
|
||||
QueueLen() int
|
||||
}
|
||||
|
||||
// SessionTokens 管理端会话令牌(nst_ 前缀,库中存 SHA-256)。
|
||||
type SessionTokens interface {
|
||||
// Issue 生成新令牌明文,返回明文与 SHA-256 十六进制(或原始哈希字节由实现约定)。
|
||||
Issue(ctx context.Context) (token string, hash []byte, err error)
|
||||
// HashToken 对已有令牌做 SHA-256(校验用,不走 argon2)。
|
||||
HashToken(token string) []byte
|
||||
// LooksLikeSessionToken 判断密码字段是否以 nst_ 开头。
|
||||
LooksLikeSessionToken(credential string) bool
|
||||
}
|
||||
|
||||
// APITokenInfo 是列表项(不含令牌明文)。
|
||||
type APITokenInfo struct {
|
||||
ID int64
|
||||
Name string
|
||||
Enabled bool
|
||||
CreatedAt time.Time
|
||||
LastUsedAt *time.Time
|
||||
}
|
||||
|
||||
// APITokens 管理后台 API 令牌(nxm_ 前缀)。
|
||||
type APITokens interface {
|
||||
Issue(ctx context.Context) (token string, hash []byte, err error)
|
||||
HashToken(token string) []byte
|
||||
LooksLikeAPIToken(credential string) bool
|
||||
}
|
||||
|
||||
// LockKind 区分锁定计数器类型。
|
||||
type LockKind string
|
||||
|
||||
const (
|
||||
// LockLoginEndpointIP:编号 + IP,5 分钟内 10 次错 → 锁该组合 5 分钟。
|
||||
LockLoginEndpointIP LockKind = "login_endpoint_ip"
|
||||
// LockLoginEndpoint:编号总数,1 小时内 50 次错 → 暂停该编号密码登录 1 小时。
|
||||
LockLoginEndpoint LockKind = "login_endpoint"
|
||||
// LockTalkPair:发送方 + 对方对话密码。
|
||||
LockTalkPair LockKind = "talk_pair"
|
||||
// LockTalkTarget:对方对话密码总数。
|
||||
LockTalkTarget LockKind = "talk_target"
|
||||
// LockAdminIP:管理员登录 / 错误 API 令牌。
|
||||
LockAdminIP LockKind = "admin_ip"
|
||||
// LockRegisterIP:注册安全码错误。
|
||||
LockRegisterIP LockKind = "register_ip"
|
||||
)
|
||||
|
||||
// LockKey 是一次锁定查询/计数的键。
|
||||
type LockKey struct {
|
||||
Kind LockKind
|
||||
EndpointID string // 端编号;管理员/注册场景可空
|
||||
PeerID string // 对话密码对方;可空
|
||||
IP string // 来源 IP;按编号总数等场景可空
|
||||
}
|
||||
|
||||
// LoginLocks 是内存锁定计数器(重启清零)。
|
||||
type LoginLocks interface {
|
||||
// Check 若当前已锁定返回 locked=true 与剩余时间。
|
||||
Check(key LockKey) (locked bool, retryAfter time.Duration)
|
||||
// Fail 记录一次失败;若因此触发锁定,返回 locked=true。
|
||||
Fail(key LockKey) (locked bool, retryAfter time.Duration)
|
||||
// ClearEndpoint 清除某端编号相关的登录锁定(两种都清),对应管理 unlock。
|
||||
ClearEndpoint(endpointID string)
|
||||
// Clear 清除精确键。
|
||||
Clear(key LockKey)
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestStubHashPoolRoundTrip(t *testing.T) {
|
||||
p := NewStubHashPool()
|
||||
h, err := p.Hash(context.Background(), PasswordLogin, "secret")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ok, err := p.Verify(context.Background(), PasswordLogin, "secret", h)
|
||||
if err != nil || !ok {
|
||||
t.Fatalf("verify: ok=%v err=%v", ok, err)
|
||||
}
|
||||
ok, _ = p.Verify(context.Background(), PasswordLogin, "wrong", h)
|
||||
if ok {
|
||||
t.Fatal("expected mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStubSessionTokenPrefix(t *testing.T) {
|
||||
s := NewStubSessionTokens()
|
||||
tok, hash, err := s.Issue(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !s.LooksLikeSessionToken(tok) {
|
||||
t.Fatalf("token %q should look like session", tok)
|
||||
}
|
||||
if len(hash) != 32 {
|
||||
t.Fatalf("hash len %d", len(hash))
|
||||
}
|
||||
}
|
||||
|
||||
func TestStubAPITokenPrefix(t *testing.T) {
|
||||
s := NewStubAPITokens()
|
||||
tok, _, err := s.Issue(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !s.LooksLikeAPIToken(tok) {
|
||||
t.Fatalf("token %q should look like api", tok)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStubLoginLocksFailRecord(t *testing.T) {
|
||||
l := NewStubLoginLocks()
|
||||
locked, _ := l.Fail(LockKey{Kind: LockLoginEndpointIP, EndpointID: "a", IP: "1.2.3.4"})
|
||||
if locked {
|
||||
t.Fatal("stub should not lock")
|
||||
}
|
||||
l.ClearEndpoint("a")
|
||||
if len(l.Cleared) != 1 || l.Cleared[0] != "a" {
|
||||
t.Fatalf("cleared=%v", l.Cleared)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// StubHashPool 是测试用假哈希池:明文加前缀,不做 argon2。
|
||||
type StubHashPool struct {
|
||||
mu sync.Mutex
|
||||
queue int
|
||||
}
|
||||
|
||||
func NewStubHashPool() *StubHashPool { return &StubHashPool{} }
|
||||
|
||||
func (p *StubHashPool) Hash(_ context.Context, _ PasswordKind, password string) (string, error) {
|
||||
return "stub$" + password, nil
|
||||
}
|
||||
|
||||
func (p *StubHashPool) Verify(_ context.Context, _ PasswordKind, password, phc string) (bool, error) {
|
||||
return phc == "stub$"+password, nil
|
||||
}
|
||||
|
||||
func (p *StubHashPool) QueueLen() int {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
return p.queue
|
||||
}
|
||||
|
||||
// StubSessionTokens 假会话令牌。
|
||||
type StubSessionTokens struct{}
|
||||
|
||||
func NewStubSessionTokens() *StubSessionTokens { return &StubSessionTokens{} }
|
||||
|
||||
func (s *StubSessionTokens) Issue(_ context.Context) (string, []byte, error) {
|
||||
tok := "nst_stub_session_token_000000000000"
|
||||
sum := sha256.Sum256([]byte(tok))
|
||||
return tok, sum[:], nil
|
||||
}
|
||||
|
||||
func (s *StubSessionTokens) HashToken(token string) []byte {
|
||||
sum := sha256.Sum256([]byte(token))
|
||||
return sum[:]
|
||||
}
|
||||
|
||||
func (s *StubSessionTokens) LooksLikeSessionToken(credential string) bool {
|
||||
return strings.HasPrefix(credential, "nst_")
|
||||
}
|
||||
|
||||
// StubAPITokens 假 API 令牌。
|
||||
type StubAPITokens struct{}
|
||||
|
||||
func NewStubAPITokens() *StubAPITokens { return &StubAPITokens{} }
|
||||
|
||||
func (s *StubAPITokens) Issue(_ context.Context) (string, []byte, error) {
|
||||
tok := "nxm_stub_api_token_0000000000000000"
|
||||
sum := sha256.Sum256([]byte(tok))
|
||||
return tok, sum[:], nil
|
||||
}
|
||||
|
||||
func (s *StubAPITokens) HashToken(token string) []byte {
|
||||
sum := sha256.Sum256([]byte(token))
|
||||
return sum[:]
|
||||
}
|
||||
|
||||
func (s *StubAPITokens) LooksLikeAPIToken(credential string) bool {
|
||||
return strings.HasPrefix(credential, "nxm_")
|
||||
}
|
||||
|
||||
// StubLoginLocks 假锁定:永不锁定,记录调用便于测试。
|
||||
type StubLoginLocks struct {
|
||||
mu sync.Mutex
|
||||
Fails []LockKey
|
||||
Cleared []string
|
||||
}
|
||||
|
||||
func NewStubLoginLocks() *StubLoginLocks { return &StubLoginLocks{} }
|
||||
|
||||
func (l *StubLoginLocks) Check(LockKey) (bool, time.Duration) { return false, 0 }
|
||||
|
||||
func (l *StubLoginLocks) Fail(key LockKey) (bool, time.Duration) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
l.Fails = append(l.Fails, key)
|
||||
return false, 0
|
||||
}
|
||||
|
||||
func (l *StubLoginLocks) ClearEndpoint(endpointID string) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
l.Cleared = append(l.Cleared, endpointID)
|
||||
}
|
||||
|
||||
func (l *StubLoginLocks) Clear(LockKey) {}
|
||||
|
||||
// 编译期检查:假实现满足接口。
|
||||
var (
|
||||
_ HashPool = (*StubHashPool)(nil)
|
||||
_ SessionTokens = (*StubSessionTokens)(nil)
|
||||
_ APITokens = (*StubAPITokens)(nil)
|
||||
_ LoginLocks = (*StubLoginLocks)(nil)
|
||||
)
|
||||
Reference in New Issue
Block a user