实现初始化验证令牌服务端解析

This commit is contained in:
xuans 2026-09-16 18:12:50 +08:00
parent ab0039998a
commit 814291773f
4 changed files with 187 additions and 0 deletions

View File

@ -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

View File

@ -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)
}
}

View File

@ -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 在线验证服务未初始化")

View File

@ -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 验证令牌仍可被解析")
}
}