优化结构
This commit is contained in:
Vendored
+15
-5
@@ -89,15 +89,23 @@ func (s *Store) Delete(ctx context.Context, key string) error {
|
||||
|
||||
func (s *Store) Increment(ctx context.Context, key string, expiration time.Duration) (int64, error) {
|
||||
if client := s.client(); client != nil {
|
||||
if expiration > 0 {
|
||||
expirationMS := expiration.Milliseconds()
|
||||
if expirationMS < 1 {
|
||||
expirationMS = 1
|
||||
}
|
||||
const incrementWithTTL = `
|
||||
local value = redis.call('INCR', KEYS[1])
|
||||
if redis.call('PTTL', KEYS[1]) < 0 then
|
||||
redis.call('PEXPIRE', KEYS[1], ARGV[1])
|
||||
end
|
||||
return value`
|
||||
return client.Eval(ctx, incrementWithTTL, []string{key}, expirationMS).Int64()
|
||||
}
|
||||
value, err := client.Incr(ctx, key).Result()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if value == 1 && expiration > 0 {
|
||||
if err := client.Expire(ctx, key, expiration).Err(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
s.mu.Lock()
|
||||
@@ -120,6 +128,8 @@ func (s *Store) Increment(ctx context.Context, key string, expiration time.Durat
|
||||
} else {
|
||||
entry.expiresAt = time.Time{}
|
||||
}
|
||||
} else if entry.expiresAt.IsZero() && expiration > 0 {
|
||||
entry.expiresAt = time.Now().Add(expiration)
|
||||
}
|
||||
s.memory[key] = entry
|
||||
return value, nil
|
||||
|
||||
+15
@@ -40,3 +40,18 @@ func TestStoreIncrementRecreatesExpiredKeyWithoutStaleExpiry(t *testing.T) {
|
||||
t.Fatalf("Get() after recreation = %q, %v, %v", value, ok, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStoreIncrementAddsTTLToExistingPersistentCounter(t *testing.T) {
|
||||
store := New(nil)
|
||||
ctx := context.Background()
|
||||
if err := store.Set(ctx, "counter", "5", 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if value, err := store.Increment(ctx, "counter", time.Millisecond); err != nil || value != 6 {
|
||||
t.Fatalf("Increment() = %d, %v", value, err)
|
||||
}
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
if _, ok, err := store.Get(ctx, "counter"); err != nil || ok {
|
||||
t.Fatalf("persistent counter did not expire: ok=%v err=%v", ok, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
package payment
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/asn1"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
@@ -20,12 +25,16 @@ const (
|
||||
var (
|
||||
appleJWSLeafExtensionOID = asn1.ObjectIdentifier{1, 2, 840, 113635, 100, 6, 11, 1}
|
||||
appleJWSIntermediateExtensionOID = asn1.ObjectIdentifier{1, 2, 840, 113635, 100, 6, 2, 1}
|
||||
appleTrustedRootFingerprints = map[string]struct{}{
|
||||
"63343abfb89a6a03ebb57e9b3f5fa7be7c4f5c756f3017b3a8c488c3653e9179": {},
|
||||
}
|
||||
)
|
||||
|
||||
// validateAppleJWSChain binds the leaf certificate used by GoPay for JWS
|
||||
// signature verification to the intermediate and root certificates that
|
||||
// GoPay validates against its embedded Apple trust anchor.
|
||||
func validateAppleJWSChain(signedPayload string) error {
|
||||
return validateAppleJWSChainWithRoots(signedPayload, appleTrustedRootFingerprints)
|
||||
}
|
||||
|
||||
func validateAppleJWSChainWithRoots(signedPayload string, trustedRoots map[string]struct{}) error {
|
||||
headerSegment, payloadSegment, signatureSegment, err := splitAppleJWS(signedPayload)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -73,6 +82,10 @@ func validateAppleJWSChain(signedPayload string) error {
|
||||
}
|
||||
certificates = append(certificates, certificate)
|
||||
}
|
||||
rootFingerprint := sha256.Sum256(certificates[2].Raw)
|
||||
if _, trusted := trustedRoots[hex.EncodeToString(rootFingerprint[:])]; !trusted {
|
||||
return errors.New("Apple JWS 根证书不受信任")
|
||||
}
|
||||
if err = certificates[0].CheckSignatureFrom(certificates[1]); err != nil {
|
||||
return fmt.Errorf("Apple JWS 叶子证书不属于声明的证书链: %w", err)
|
||||
}
|
||||
@@ -101,19 +114,20 @@ func validateAppleJWSChain(signedPayload string) error {
|
||||
return errors.New("Apple JWS 中间证书缺少 Apple 签名扩展")
|
||||
}
|
||||
|
||||
if hasVerificationTime {
|
||||
roots := x509.NewCertPool()
|
||||
roots.AddCert(certificates[2])
|
||||
intermediates := x509.NewCertPool()
|
||||
intermediates.AddCert(certificates[1])
|
||||
if _, err = certificates[0].Verify(x509.VerifyOptions{
|
||||
Roots: roots,
|
||||
Intermediates: intermediates,
|
||||
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
|
||||
CurrentTime: verificationTime,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("验证 Apple JWS 证书链: %w", err)
|
||||
}
|
||||
if !hasVerificationTime {
|
||||
verificationTime = time.Now()
|
||||
}
|
||||
roots := x509.NewCertPool()
|
||||
roots.AddCert(certificates[2])
|
||||
intermediates := x509.NewCertPool()
|
||||
intermediates.AddCert(certificates[1])
|
||||
if _, err = certificates[0].Verify(x509.VerifyOptions{
|
||||
Roots: roots,
|
||||
Intermediates: intermediates,
|
||||
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
|
||||
CurrentTime: verificationTime,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("验证 Apple JWS 证书链: %w", err)
|
||||
}
|
||||
if certificates[0].IsCA {
|
||||
return errors.New("Apple JWS 叶子证书不能是 CA")
|
||||
@@ -121,6 +135,18 @@ func validateAppleJWSChain(signedPayload string) error {
|
||||
if certificates[0].KeyUsage != 0 && certificates[0].KeyUsage&x509.KeyUsageDigitalSignature == 0 {
|
||||
return errors.New("Apple JWS 叶子证书不允许数字签名")
|
||||
}
|
||||
publicKey, ok := certificates[0].PublicKey.(*ecdsa.PublicKey)
|
||||
if !ok || publicKey.Curve != elliptic.P256() {
|
||||
return errors.New("Apple JWS 叶子证书不是 P-256 ECDSA 公钥")
|
||||
}
|
||||
signature, err := base64.RawURLEncoding.DecodeString(signatureSegment)
|
||||
if err != nil || len(signature) != 64 {
|
||||
return errors.New("Apple JWS ES256 签名格式无效")
|
||||
}
|
||||
digest := sha256.Sum256([]byte(headerSegment + "." + payloadSegment))
|
||||
if !ecdsa.Verify(publicKey, digest[:], new(big.Int).SetBytes(signature[:32]), new(big.Int).SetBytes(signature[32:])) {
|
||||
return errors.New("Apple JWS 签名校验失败")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -4,10 +4,12 @@ import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/asn1"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"math/big"
|
||||
"testing"
|
||||
@@ -16,7 +18,8 @@ import (
|
||||
|
||||
func TestValidateAppleJWSChain(t *testing.T) {
|
||||
chain := testAppleCertificateChain(t)
|
||||
if err := validateAppleJWSChain(testAppleJWS(t, "ES256", chain, time.Now())); err != nil {
|
||||
jws := testAppleJWS(t, "ES256", chain, time.Now())
|
||||
if err := validateAppleJWSChainWithRoots(jws, testAppleTrustedRoots(t, chain)); err != nil {
|
||||
t.Fatalf("validateAppleJWSChain() error = %v", err)
|
||||
}
|
||||
}
|
||||
@@ -25,17 +28,30 @@ func TestValidateAppleJWSChainRejectsUnrelatedLeaf(t *testing.T) {
|
||||
chain := testAppleCertificateChain(t)
|
||||
attackerChain := testAppleCertificateChain(t)
|
||||
chain[0] = attackerChain[0]
|
||||
if err := validateAppleJWSChain(testAppleJWS(t, "ES256", chain, time.Now())); err == nil {
|
||||
if err := validateAppleJWSChainWithRoots(testAppleJWS(t, "ES256", chain, time.Now()), testAppleTrustedRoots(t, chain)); err == nil {
|
||||
t.Fatal("validateAppleJWSChain() accepted a leaf outside the declared chain")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateAppleJWSChainRejectsInvalidSignature(t *testing.T) {
|
||||
chain := testAppleCertificateChain(t)
|
||||
jws := testAppleJWS(t, "ES256", chain, time.Now())
|
||||
header, payload, _, err := splitAppleJWS(jws)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tampered := header + "." + payload + "." + base64.RawURLEncoding.EncodeToString(make([]byte, 64))
|
||||
if err = validateAppleJWSChainWithRoots(tampered, testAppleTrustedRoots(t, chain)); err == nil {
|
||||
t.Fatal("validateAppleJWSChain accepted an invalid ES256 signature")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateAppleJWSChainRejectsUnexpectedAlgorithmAndShape(t *testing.T) {
|
||||
chain := testAppleCertificateChain(t)
|
||||
if err := validateAppleJWSChain(testAppleJWS(t, "ES384", chain, time.Now())); err == nil {
|
||||
if err := validateAppleJWSChainWithRoots(testAppleJWS(t, "ES384", chain, time.Now()), testAppleTrustedRoots(t, chain)); err == nil {
|
||||
t.Fatal("validateAppleJWSChain() accepted a non-ES256 algorithm")
|
||||
}
|
||||
if err := validateAppleJWSChain(testAppleJWS(t, "ES256", chain[:2], time.Now())); err == nil {
|
||||
if err := validateAppleJWSChainWithRoots(testAppleJWS(t, "ES256", chain[:2], time.Now()), nil); err == nil {
|
||||
t.Fatal("validateAppleJWSChain() accepted an incomplete x5c chain")
|
||||
}
|
||||
}
|
||||
@@ -43,14 +59,14 @@ func TestValidateAppleJWSChainRejectsUnexpectedAlgorithmAndShape(t *testing.T) {
|
||||
func TestValidateAppleJWSChainRejectsAdditionalCertificates(t *testing.T) {
|
||||
chain := testAppleCertificateChain(t)
|
||||
chain = append(chain, chain[2])
|
||||
if err := validateAppleJWSChain(testAppleJWS(t, "ES256", chain, time.Now())); err == nil {
|
||||
if err := validateAppleJWSChainWithRoots(testAppleJWS(t, "ES256", chain, time.Now()), testAppleTrustedRoots(t, chain[:3])); err == nil {
|
||||
t.Fatal("validateAppleJWSChain() accepted additional x5c certificates")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateAppleJWSChainRejectsNonAppleSigningCertificates(t *testing.T) {
|
||||
chain := testAppleCertificateChainWithoutAppleExtensions(t)
|
||||
if err := validateAppleJWSChain(testAppleJWS(t, "ES256", chain, time.Now())); err == nil {
|
||||
if err := validateAppleJWSChainWithRoots(testAppleJWS(t, "ES256", chain, time.Now()), testAppleTrustedRoots(t, chain)); err == nil {
|
||||
t.Fatal("validateAppleJWSChain() accepted certificates without Apple signing extensions")
|
||||
}
|
||||
}
|
||||
@@ -58,7 +74,7 @@ func TestValidateAppleJWSChainRejectsNonAppleSigningCertificates(t *testing.T) {
|
||||
func TestValidateAppleJWSChainUsesSignedDateForHistoricalPayload(t *testing.T) {
|
||||
signedAt := time.Now().AddDate(-2, 0, 0)
|
||||
chain := testAppleCertificateChainAt(t, signedAt, true)
|
||||
if err := validateAppleJWSChain(testAppleJWS(t, "ES256", chain, signedAt)); err != nil {
|
||||
if err := validateAppleJWSChainWithRoots(testAppleJWS(t, "ES256", chain, signedAt), testAppleTrustedRoots(t, chain)); err != nil {
|
||||
t.Fatalf("validateAppleJWSChain() rejected historical payload: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -78,17 +94,17 @@ func TestValidateAppleJWSChainUsesNestedSignedDateForNotification(t *testing.T)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
outer := base64.RawURLEncoding.EncodeToString(header) + "." + base64.RawURLEncoding.EncodeToString(payload) + ".signature"
|
||||
if err := validateAppleJWSChain(outer); err != nil {
|
||||
outer := testAppleSignedJWS(t, header, payload, chain)
|
||||
if err := validateAppleJWSChainWithRoots(outer, testAppleTrustedRoots(t, chain)); err != nil {
|
||||
t.Fatalf("validateAppleJWSChain() rejected historical notification: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateAppleJWSChainAcceptsExpiredChainWithoutSignedDate(t *testing.T) {
|
||||
func TestValidateAppleJWSChainRejectsExpiredChainWithoutSignedDate(t *testing.T) {
|
||||
signedAt := time.Now().AddDate(-2, 0, 0)
|
||||
chain := testAppleCertificateChainAt(t, signedAt, true)
|
||||
if err := validateAppleJWSChain(testAppleJWSWithoutDate(t, chain)); err != nil {
|
||||
t.Fatalf("validateAppleJWSChain() rejected an expired chain without signedDate: %v", err)
|
||||
if err := validateAppleJWSChainWithRoots(testAppleJWSWithoutDate(t, chain), testAppleTrustedRoots(t, chain)); err == nil {
|
||||
t.Fatal("validateAppleJWSChain() accepted an expired chain without signedDate")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,7 +112,7 @@ func TestValidateAppleJWSChainRejectsBrokenChainWithoutSignedDate(t *testing.T)
|
||||
chain := testAppleCertificateChain(t)
|
||||
attackerChain := testAppleCertificateChain(t)
|
||||
chain[0] = attackerChain[0]
|
||||
if err := validateAppleJWSChain(testAppleJWSWithoutDate(t, chain)); err == nil {
|
||||
if err := validateAppleJWSChainWithRoots(testAppleJWSWithoutDate(t, chain), testAppleTrustedRoots(t, chain)); err == nil {
|
||||
t.Fatal("validateAppleJWSChain() accepted a broken chain without signedDate")
|
||||
}
|
||||
}
|
||||
@@ -169,11 +185,13 @@ func testAppleCertificateChainAt(t *testing.T, validAt time.Time, includeAppleEx
|
||||
}}
|
||||
}
|
||||
leafDER := testAppleCertificate(t, leafTemplate, intermediate, &leafKey.PublicKey, intermediateKey)
|
||||
return []string{
|
||||
chain := []string{
|
||||
base64.StdEncoding.EncodeToString(leafDER),
|
||||
base64.StdEncoding.EncodeToString(intermediateDER),
|
||||
base64.StdEncoding.EncodeToString(rootDER),
|
||||
}
|
||||
testAppleLeafKeys[chain[0]] = leafKey
|
||||
return chain
|
||||
}
|
||||
|
||||
func testAppleECDSAKey(t *testing.T) *ecdsa.PrivateKey {
|
||||
@@ -204,7 +222,7 @@ func testAppleJWS(t *testing.T, algorithm string, chain []string, signedAt time.
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return base64.RawURLEncoding.EncodeToString(header) + "." + base64.RawURLEncoding.EncodeToString(payload) + ".signature"
|
||||
return testAppleSignedJWS(t, header, payload, chain)
|
||||
}
|
||||
|
||||
func testAppleJWSWithoutDate(t *testing.T, chain []string) string {
|
||||
@@ -217,5 +235,36 @@ func testAppleJWSWithoutDate(t *testing.T, chain []string) string {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return base64.RawURLEncoding.EncodeToString(header) + "." + base64.RawURLEncoding.EncodeToString(payload) + ".signature"
|
||||
return testAppleSignedJWS(t, header, payload, chain)
|
||||
}
|
||||
|
||||
var testAppleLeafKeys = map[string]*ecdsa.PrivateKey{}
|
||||
|
||||
func testAppleSignedJWS(t *testing.T, header, payload []byte, chain []string) string {
|
||||
t.Helper()
|
||||
headerSegment := base64.RawURLEncoding.EncodeToString(header)
|
||||
payloadSegment := base64.RawURLEncoding.EncodeToString(payload)
|
||||
key := testAppleLeafKeys[chain[0]]
|
||||
if key == nil {
|
||||
t.Fatal("missing test Apple leaf key")
|
||||
}
|
||||
digest := sha256.Sum256([]byte(headerSegment + "." + payloadSegment))
|
||||
r, s, err := ecdsa.Sign(rand.Reader, key, digest[:])
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
signature := make([]byte, 64)
|
||||
r.FillBytes(signature[:32])
|
||||
s.FillBytes(signature[32:])
|
||||
return headerSegment + "." + payloadSegment + "." + base64.RawURLEncoding.EncodeToString(signature)
|
||||
}
|
||||
|
||||
func testAppleTrustedRoots(t *testing.T, chain []string) map[string]struct{} {
|
||||
t.Helper()
|
||||
rootDER, err := base64.StdEncoding.DecodeString(chain[2])
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fingerprint := sha256.Sum256(rootDER)
|
||||
return map[string]struct{}{hex.EncodeToString(fingerprint[:]): {}}
|
||||
}
|
||||
|
||||
@@ -56,17 +56,11 @@ func first(values map[string]string, keys ...string) string {
|
||||
}
|
||||
|
||||
func text(values map[string]any, key string) string {
|
||||
value, _ := values[key].(string)
|
||||
return value
|
||||
return paymentutil.Text(values, key)
|
||||
}
|
||||
|
||||
func firstAny(values map[string]any, keys ...string) string {
|
||||
for _, key := range keys {
|
||||
if value, ok := values[key].(string); ok && value != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
return paymentutil.FirstText(values, keys...)
|
||||
}
|
||||
|
||||
// Compatibility shims keep channel adapters focused on protocol and
|
||||
|
||||
@@ -133,6 +133,35 @@ func (a *vendorPaymentAdapter) call(ctx context.Context, endpointKey string, pay
|
||||
}
|
||||
result := &bizpayment.PaymentResult{Provider: a.provider, Status: "created", TradeNo: tradeNo, Payload: ensureJSON(body)}
|
||||
if endpointKey != "query_url" {
|
||||
if object := jsonObject(body); object != nil {
|
||||
operation := strings.TrimSuffix(endpointKey, "_url")
|
||||
statusField := text(c, operation+"_status_field")
|
||||
if statusField == "" {
|
||||
statusField = text(c, "query_status_field")
|
||||
}
|
||||
state := ""
|
||||
if statusField != "" {
|
||||
state = stringAtPath(object, statusField)
|
||||
}
|
||||
if state == "" {
|
||||
for _, key := range []string{"status", "trade_status", "order_status", "pay_status", "refund_status"} {
|
||||
if value := stringAtPath(object, key); value != "" {
|
||||
state = value
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if state != "" {
|
||||
switch {
|
||||
case containsFold(configuredValues(c, operation+"_failure_values"), state):
|
||||
result.Status = "failed"
|
||||
case containsFold(configuredValues(c, operation+"_success_values"), state):
|
||||
result.Status = "created"
|
||||
default:
|
||||
result.Status = normalizePaymentStatus(state, "created")
|
||||
}
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
object := jsonObject(body)
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
package payment
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
bizpayment "kra/internal/biz/payment"
|
||||
)
|
||||
|
||||
func TestChinaumsCallbackAuthorizationVerification(t *testing.T) {
|
||||
@@ -31,3 +36,23 @@ func TestChinaumsCallbackAuthorizationVerification(t *testing.T) {
|
||||
t.Fatalf("mismatched Chinaums AppId error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVendorRefundMapsBusinessFailureStatus(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"status":"FAIL","message":"rejected"}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
adapter := &vendorPaymentAdapter{provider: "vendor", profile: vendorSFT}
|
||||
result, err := adapter.Refund(context.Background(), &bizpayment.PaymentRefundRequest{TradeNo: "trade-1", RefundNo: "refund-1", Amount: 100, TotalAmount: 100, Currency: "CNY"}, map[string]any{
|
||||
"refund_url": server.URL,
|
||||
"app_key": "secret",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Refund() error = %v", err)
|
||||
}
|
||||
if result == nil || result.Status != "failed" {
|
||||
t.Fatalf("Refund() result = %#v, want failed status", result)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user