实现初始化验证令牌服务端解析
This commit is contained in:
parent
ab0039998a
commit
814291773f
@ -61,6 +61,14 @@ type databaseVerificationSession struct {
|
|||||||
ExpiresAt time.Time
|
ExpiresAt time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DatabaseVerificationSession 仅供服务端完整初始化事务读取验证结果。
|
||||||
|
// 所有字段均禁止 JSON 序列化,避免凭据引用被误写入 HTTP 响应。
|
||||||
|
type DatabaseVerificationSession struct {
|
||||||
|
Config config.DatabaseConfig `json:"-"`
|
||||||
|
CredentialRef string `json:"-"`
|
||||||
|
ExpiresAt time.Time `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
type DatabaseVerificationManager struct {
|
type DatabaseVerificationManager struct {
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
base config.Config
|
base config.Config
|
||||||
@ -150,6 +158,19 @@ func (m *DatabaseVerificationManager) Consume(token, credentialRef string) (conf
|
|||||||
return m.session(token, credentialRef, true)
|
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 按验证令牌精确回收尚未消费的数据库凭据,不影响其他初始化会话。
|
// Discard 按验证令牌精确回收尚未消费的数据库凭据,不影响其他初始化会话。
|
||||||
func (m *DatabaseVerificationManager) Discard(token string) error {
|
func (m *DatabaseVerificationManager) Discard(token string) error {
|
||||||
if m == nil {
|
if m == nil {
|
||||||
@ -207,6 +228,26 @@ func (m *DatabaseVerificationManager) session(token, credentialRef string, consu
|
|||||||
return session.Config, nil
|
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() {
|
func (m *DatabaseVerificationManager) DiscardAll() {
|
||||||
if m == nil {
|
if m == nil {
|
||||||
return
|
return
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package bootstrap
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@ -198,3 +199,70 @@ func TestDatabaseVerificationDiscardOnlyRemovesSelectedToken(t *testing.T) {
|
|||||||
t.Fatal("数据库验证令牌被重复回收")
|
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
|
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 {
|
type DNSVerificationManager struct {
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
tester DNSCredentialTester
|
tester DNSCredentialTester
|
||||||
@ -172,6 +181,20 @@ func (m *DNSVerificationManager) Consume(token, providerID, credentialRef, testD
|
|||||||
return m.validateSession(token, providerID, credentialRef, testDomain, true)
|
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 临时凭据,不影响其他初始化会话。
|
// Discard 按验证令牌精确回收尚未消费的 DNS 临时凭据,不影响其他初始化会话。
|
||||||
func (m *DNSVerificationManager) Discard(token string) error {
|
func (m *DNSVerificationManager) Discard(token string) error {
|
||||||
if m == nil {
|
if m == nil {
|
||||||
@ -249,6 +272,26 @@ func (m *DNSVerificationManager) validateSession(token, providerID, credentialRe
|
|||||||
return nil
|
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 {
|
func (m *DNSVerificationManager) reserveVerificationSlot() error {
|
||||||
if m == nil {
|
if m == nil {
|
||||||
return errors.New("DNS 在线验证服务未初始化")
|
return errors.New("DNS 在线验证服务未初始化")
|
||||||
|
|||||||
@ -2,7 +2,9 @@ package setup
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@ -357,3 +359,36 @@ func TestDNSVerificationManagerNormalizesCredentialCopyBeforeTestingAndStoring(t
|
|||||||
t.Fatal("规范化过程不应修改调用方的原始凭据 Map")
|
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