实现初始化验证令牌服务端解析
This commit is contained in:
parent
ab0039998a
commit
814291773f
@ -61,6 +61,14 @@ type databaseVerificationSession struct {
|
||||
ExpiresAt time.Time
|
||||
}
|
||||
|
||||
// DatabaseVerificationSession 仅供服务端完整初始化事务读取验证结果。
|
||||
// 所有字段均禁止 JSON 序列化,避免凭据引用被误写入 HTTP 响应。
|
||||
type DatabaseVerificationSession struct {
|
||||
Config config.DatabaseConfig `json:"-"`
|
||||
CredentialRef string `json:"-"`
|
||||
ExpiresAt time.Time `json:"-"`
|
||||
}
|
||||
|
||||
type DatabaseVerificationManager struct {
|
||||
mu sync.Mutex
|
||||
base config.Config
|
||||
@ -150,6 +158,19 @@ func (m *DatabaseVerificationManager) Consume(token, credentialRef string) (conf
|
||||
return m.session(token, credentialRef, true)
|
||||
}
|
||||
|
||||
// Resolve 按验证令牌读取完整初始化所需的服务端状态,但不消费令牌。
|
||||
func (m *DatabaseVerificationManager) Resolve(token string) (DatabaseVerificationSession, error) {
|
||||
session, err := m.resolveSession(token)
|
||||
if err != nil {
|
||||
return DatabaseVerificationSession{}, err
|
||||
}
|
||||
return DatabaseVerificationSession{
|
||||
Config: session.Config,
|
||||
CredentialRef: session.CredentialRef,
|
||||
ExpiresAt: session.ExpiresAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Discard 按验证令牌精确回收尚未消费的数据库凭据,不影响其他初始化会话。
|
||||
func (m *DatabaseVerificationManager) Discard(token string) error {
|
||||
if m == nil {
|
||||
@ -207,6 +228,26 @@ func (m *DatabaseVerificationManager) session(token, credentialRef string, consu
|
||||
return session.Config, nil
|
||||
}
|
||||
|
||||
func (m *DatabaseVerificationManager) resolveSession(token string) (databaseVerificationSession, error) {
|
||||
if m == nil {
|
||||
return databaseVerificationSession{}, errors.New("数据库验证服务尚未初始化")
|
||||
}
|
||||
token = strings.TrimSpace(token)
|
||||
if token == "" {
|
||||
return databaseVerificationSession{}, errors.New("必须先完成数据库连接和写权限测试")
|
||||
}
|
||||
now := m.now().UTC()
|
||||
m.mu.Lock()
|
||||
expired := m.clearExpiredLocked(now)
|
||||
session, ok := m.sessions[token]
|
||||
m.mu.Unlock()
|
||||
m.deleteCredentials(expired)
|
||||
if !ok {
|
||||
return databaseVerificationSession{}, errors.New("数据库验证令牌无效或已经过期,请重新测试")
|
||||
}
|
||||
return session, nil
|
||||
}
|
||||
|
||||
func (m *DatabaseVerificationManager) DiscardAll() {
|
||||
if m == nil {
|
||||
return
|
||||
|
||||
@ -2,6 +2,7 @@ package bootstrap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
@ -198,3 +199,70 @@ func TestDatabaseVerificationDiscardOnlyRemovesSelectedToken(t *testing.T) {
|
||||
t.Fatal("数据库验证令牌被重复回收")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDatabaseVerificationResolveIsServerOnlyAndDoesNotConsumeToken(t *testing.T) {
|
||||
base := config.Default(t.TempDir())
|
||||
store := &memoryDatabaseStore{}
|
||||
manager := NewDatabaseVerificationManager(base, store)
|
||||
manager.verify = func(context.Context, config.DatabaseConfig) error { return nil }
|
||||
result, err := manager.Verify(t.Context(), DatabaseInput{
|
||||
Driver: "mysql", Host: "db.example.test", Database: "scert",
|
||||
Username: "scert_user", Password: "database-password", TLSMode: "true",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
resolved, err := manager.Resolve(result.VerificationToken)
|
||||
if err != nil {
|
||||
t.Fatalf("按令牌解析数据库验证结果失败:%v", err)
|
||||
}
|
||||
if resolved.Config.CredentialRef != result.CredentialRef || resolved.CredentialRef != result.CredentialRef || resolved.ExpiresAt != result.ExpiresAt {
|
||||
t.Fatalf("解析的数据库验证状态不完整:%+v", resolved)
|
||||
}
|
||||
encoded, err := json.Marshal(resolved)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Contains(string(encoded), result.CredentialRef) || strings.Contains(string(encoded), result.Config.DSN) {
|
||||
t.Fatalf("服务端数据库验证状态可被意外序列化:%s", encoded)
|
||||
}
|
||||
if _, err := manager.Consume(result.VerificationToken, result.CredentialRef); err != nil {
|
||||
t.Fatalf("Resolve 不应消费数据库验证令牌:%v", err)
|
||||
}
|
||||
if _, err := manager.Resolve(result.VerificationToken); err == nil {
|
||||
t.Fatal("已消费的数据库验证令牌仍可被解析")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDatabaseVerificationConsumesTokenOnlyOnceUnderConcurrency(t *testing.T) {
|
||||
base := config.Default(t.TempDir())
|
||||
store := &memoryDatabaseStore{}
|
||||
manager := NewDatabaseVerificationManager(base, store)
|
||||
manager.verify = func(context.Context, config.DatabaseConfig) error { return nil }
|
||||
result, err := manager.Verify(t.Context(), DatabaseInput{
|
||||
Driver: "mysql", Host: "db.example.test", Database: "scert",
|
||||
Username: "scert_user", Password: "database-password", TLSMode: "true",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
start := make(chan struct{})
|
||||
results := make(chan error, 2)
|
||||
for range 2 {
|
||||
go func() {
|
||||
<-start
|
||||
_, consumeErr := manager.Consume(result.VerificationToken, result.CredentialRef)
|
||||
results <- consumeErr
|
||||
}()
|
||||
}
|
||||
close(start)
|
||||
successes := 0
|
||||
for range 2 {
|
||||
if <-results == nil {
|
||||
successes++
|
||||
}
|
||||
}
|
||||
if successes != 1 {
|
||||
t.Fatalf("并发消费数据库验证令牌成功次数为 %d,预期为 1", successes)
|
||||
}
|
||||
}
|
||||
|
||||
@ -79,6 +79,15 @@ type dnsVerificationSession struct {
|
||||
ExpiresAt time.Time
|
||||
}
|
||||
|
||||
// DNSVerificationSession 仅供服务端完整初始化事务读取验证结果。
|
||||
// 所有字段均禁止 JSON 序列化,避免凭据引用被误写入 HTTP 响应。
|
||||
type DNSVerificationSession struct {
|
||||
ProviderID string `json:"-"`
|
||||
TestDomain string `json:"-"`
|
||||
CredentialRef string `json:"-"`
|
||||
ExpiresAt time.Time `json:"-"`
|
||||
}
|
||||
|
||||
type DNSVerificationManager struct {
|
||||
mu sync.Mutex
|
||||
tester DNSCredentialTester
|
||||
@ -172,6 +181,20 @@ func (m *DNSVerificationManager) Consume(token, providerID, credentialRef, testD
|
||||
return m.validateSession(token, providerID, credentialRef, testDomain, true)
|
||||
}
|
||||
|
||||
// Resolve 按验证令牌读取完整初始化所需的服务端状态,但不消费令牌。
|
||||
func (m *DNSVerificationManager) Resolve(token string) (DNSVerificationSession, error) {
|
||||
session, err := m.resolveSession(token)
|
||||
if err != nil {
|
||||
return DNSVerificationSession{}, err
|
||||
}
|
||||
return DNSVerificationSession{
|
||||
ProviderID: session.ProviderID,
|
||||
TestDomain: session.TestDomain,
|
||||
CredentialRef: session.CredentialRef,
|
||||
ExpiresAt: session.ExpiresAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Discard 按验证令牌精确回收尚未消费的 DNS 临时凭据,不影响其他初始化会话。
|
||||
func (m *DNSVerificationManager) Discard(token string) error {
|
||||
if m == nil {
|
||||
@ -249,6 +272,26 @@ func (m *DNSVerificationManager) validateSession(token, providerID, credentialRe
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *DNSVerificationManager) resolveSession(token string) (dnsVerificationSession, error) {
|
||||
if m == nil {
|
||||
return dnsVerificationSession{}, errors.New("DNS 在线验证服务未初始化")
|
||||
}
|
||||
token = strings.TrimSpace(token)
|
||||
if token == "" {
|
||||
return dnsVerificationSession{}, errors.New("必须先完成 DNS API 在线验证")
|
||||
}
|
||||
now := m.now().UTC()
|
||||
m.mu.Lock()
|
||||
expiredRefs := m.clearExpiredLocked(now)
|
||||
session, ok := m.sessions[token]
|
||||
m.mu.Unlock()
|
||||
m.deleteCredentials(expiredRefs)
|
||||
if !ok {
|
||||
return dnsVerificationSession{}, errors.New("DNS 验证令牌无效或已经过期,请重新测试")
|
||||
}
|
||||
return session, nil
|
||||
}
|
||||
|
||||
func (m *DNSVerificationManager) reserveVerificationSlot() error {
|
||||
if m == nil {
|
||||
return errors.New("DNS 在线验证服务未初始化")
|
||||
|
||||
@ -2,7 +2,9 @@ package setup
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
@ -357,3 +359,36 @@ func TestDNSVerificationManagerNormalizesCredentialCopyBeforeTestingAndStoring(t
|
||||
t.Fatal("规范化过程不应修改调用方的原始凭据 Map")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDNSVerificationResolveIsServerOnlyAndDoesNotConsumeToken(t *testing.T) {
|
||||
store := &memorySecretWriter{}
|
||||
manager := NewDNSVerificationManager(fakeDNSTester{}, store)
|
||||
result, err := manager.Verify(t.Context(), DNSVerificationInput{
|
||||
ProviderID: "aliyun", TestDomain: "www.example.com",
|
||||
Credentials: map[string]string{"access_key_id": "test-id", "access_key_secret": "test-secret"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
resolved, err := manager.Resolve(result.VerificationToken)
|
||||
if err != nil {
|
||||
t.Fatalf("按令牌解析 DNS 验证结果失败:%v", err)
|
||||
}
|
||||
if resolved.ProviderID != "aliyun" || resolved.TestDomain != "www.example.com" ||
|
||||
resolved.CredentialRef != result.CredentialRef || resolved.ExpiresAt != result.ExpiresAt {
|
||||
t.Fatalf("解析的 DNS 验证状态不完整:%+v", resolved)
|
||||
}
|
||||
encoded, err := json.Marshal(resolved)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Contains(string(encoded), result.CredentialRef) || strings.Contains(string(encoded), resolved.TestDomain) {
|
||||
t.Fatalf("服务端 DNS 验证状态可被意外序列化:%s", encoded)
|
||||
}
|
||||
if err := manager.Consume(result.VerificationToken, resolved.ProviderID, resolved.CredentialRef, resolved.TestDomain); err != nil {
|
||||
t.Fatalf("Resolve 不应消费 DNS 验证令牌:%v", err)
|
||||
}
|
||||
if _, err := manager.Resolve(result.VerificationToken); err == nil {
|
||||
t.Fatal("已消费的 DNS 验证令牌仍可被解析")
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user