You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@apisix.apache.org by al...@apache.org on 2026/05/13 06:16:29 UTC

(apisix-ingress-controller) branch master updated: feat: support plugins field in ApisixConsumer (#2761)

This is an automated email from the ASF dual-hosted git repository.

AlinsRan pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix-ingress-controller.git


The following commit(s) were added to refs/heads/master by this push:
     new 336f4270 feat: support plugins field in ApisixConsumer (#2761)
336f4270 is described below

commit 336f427059cae1f9eaf62895da204a89a22562f3
Author: AlinsRan <al...@apache.org>
AuthorDate: Wed May 13 14:16:23 2026 +0800

    feat: support plugins field in ApisixConsumer (#2761)
---
 api/v2/apisixconsumer_types.go                     |   8 +-
 api/v2/apisixconsumer_validation_test.go           |  24 +--
 api/v2/zz_generated.deepcopy.go                    |  13 +-
 .../bases/apisix.apache.org_apisixconsumers.yaml   |  28 +++-
 docs/en/latest/reference/api-reference.md          |   2 +
 internal/adc/translator/apisixconsumer.go          | 100 +++++++----
 internal/controller/apisixconsumer_controller.go   |  77 ++++++---
 internal/controller/indexer/indexer.go             |  35 ++--
 internal/webhook/v1/apisixconsumer_webhook.go      |  37 ++--
 internal/webhook/v1/apisixconsumer_webhook_test.go |  10 +-
 test/e2e/crds/v2/consumer.go                       | 186 +++++++++++++++++++++
 11 files changed, 404 insertions(+), 116 deletions(-)

diff --git a/api/v2/apisixconsumer_types.go b/api/v2/apisixconsumer_types.go
index 7a05f9ca..03d1cfce 100644
--- a/api/v2/apisixconsumer_types.go
+++ b/api/v2/apisixconsumer_types.go
@@ -29,7 +29,13 @@ type ApisixConsumerSpec struct {
 	IngressClassName string `json:"ingressClassName,omitempty" yaml:"ingressClassName,omitempty"`
 
 	// AuthParameter defines the authentication credentials and configuration for this consumer.
-	AuthParameter ApisixConsumerAuthParameter `json:"authParameter" yaml:"authParameter"`
+	// +kubebuilder:validation:Optional
+	AuthParameter *ApisixConsumerAuthParameter `json:"authParameter,omitempty" yaml:"authParameter,omitempty"`
+
+	// Plugins lists additional consumer-scoped plugins to attach to this consumer.
+	// These plugins are applied alongside any authentication plugin derived from AuthParameter.
+	// An enabled plugin with the same name as the auth plugin derived from AuthParameter takes precedence.
+	Plugins []ApisixRoutePlugin `json:"plugins,omitempty" yaml:"plugins,omitempty"`
 }
 
 // ApisixConsumerStatus defines the observed state of ApisixConsumer.
diff --git a/api/v2/apisixconsumer_validation_test.go b/api/v2/apisixconsumer_validation_test.go
index 88fdd1d6..5c421315 100644
--- a/api/v2/apisixconsumer_validation_test.go
+++ b/api/v2/apisixconsumer_validation_test.go
@@ -109,7 +109,7 @@ func TestApisixConsumer_JwtAuth_SymmetricHS256(t *testing.T) {
 	v := loadApisixConsumerSchema(t)
 	ac := &apisixv2.ApisixConsumer{
 		Spec: apisixv2.ApisixConsumerSpec{
-			AuthParameter: apisixv2.ApisixConsumerAuthParameter{
+			AuthParameter: &apisixv2.ApisixConsumerAuthParameter{
 				JwtAuth: &apisixv2.ApisixConsumerJwtAuth{
 					Value: &apisixv2.ApisixConsumerJwtAuthValue{
 						Key:       "my-key",
@@ -130,7 +130,7 @@ func TestApisixConsumer_JwtAuth_AsymmetricWithWhitespaceOnlyPublicKey(t *testing
 	v := loadApisixConsumerSchema(t)
 	ac := &apisixv2.ApisixConsumer{
 		Spec: apisixv2.ApisixConsumerSpec{
-			AuthParameter: apisixv2.ApisixConsumerAuthParameter{
+			AuthParameter: &apisixv2.ApisixConsumerAuthParameter{
 				JwtAuth: &apisixv2.ApisixConsumerJwtAuth{
 					Value: &apisixv2.ApisixConsumerJwtAuthValue{
 						Key:       "my-key",
@@ -150,7 +150,7 @@ func TestApisixConsumer_JwtAuth_SymmetricHS512(t *testing.T) {
 	v := loadApisixConsumerSchema(t)
 	ac := &apisixv2.ApisixConsumer{
 		Spec: apisixv2.ApisixConsumerSpec{
-			AuthParameter: apisixv2.ApisixConsumerAuthParameter{
+			AuthParameter: &apisixv2.ApisixConsumerAuthParameter{
 				JwtAuth: &apisixv2.ApisixConsumerJwtAuth{
 					Value: &apisixv2.ApisixConsumerJwtAuthValue{
 						Key:       "my-key",
@@ -168,7 +168,7 @@ func TestApisixConsumer_JwtAuth_NoAlgorithmDefaultsToSymmetric(t *testing.T) {
 	v := loadApisixConsumerSchema(t)
 	ac := &apisixv2.ApisixConsumer{
 		Spec: apisixv2.ApisixConsumerSpec{
-			AuthParameter: apisixv2.ApisixConsumerAuthParameter{
+			AuthParameter: &apisixv2.ApisixConsumerAuthParameter{
 				JwtAuth: &apisixv2.ApisixConsumerJwtAuth{
 					Value: &apisixv2.ApisixConsumerJwtAuthValue{
 						Key:    "my-key",
@@ -185,7 +185,7 @@ func TestApisixConsumer_JwtAuth_AsymmetricRS256WithPublicKey(t *testing.T) {
 	v := loadApisixConsumerSchema(t)
 	ac := &apisixv2.ApisixConsumer{
 		Spec: apisixv2.ApisixConsumerSpec{
-			AuthParameter: apisixv2.ApisixConsumerAuthParameter{
+			AuthParameter: &apisixv2.ApisixConsumerAuthParameter{
 				JwtAuth: &apisixv2.ApisixConsumerJwtAuth{
 					Value: &apisixv2.ApisixConsumerJwtAuthValue{
 						Key:       "my-key",
@@ -203,7 +203,7 @@ func TestApisixConsumer_JwtAuth_AsymmetricRS256WithPrivateKey(t *testing.T) {
 	v := loadApisixConsumerSchema(t)
 	ac := &apisixv2.ApisixConsumer{
 		Spec: apisixv2.ApisixConsumerSpec{
-			AuthParameter: apisixv2.ApisixConsumerAuthParameter{
+			AuthParameter: &apisixv2.ApisixConsumerAuthParameter{
 				JwtAuth: &apisixv2.ApisixConsumerJwtAuth{
 					Value: &apisixv2.ApisixConsumerJwtAuthValue{
 						Key:        "my-key",
@@ -221,7 +221,7 @@ func TestApisixConsumer_JwtAuth_AsymmetricRS256WithBothKeys(t *testing.T) {
 	v := loadApisixConsumerSchema(t)
 	ac := &apisixv2.ApisixConsumer{
 		Spec: apisixv2.ApisixConsumerSpec{
-			AuthParameter: apisixv2.ApisixConsumerAuthParameter{
+			AuthParameter: &apisixv2.ApisixConsumerAuthParameter{
 				JwtAuth: &apisixv2.ApisixConsumerJwtAuth{
 					Value: &apisixv2.ApisixConsumerJwtAuthValue{
 						Key:        "my-key",
@@ -240,7 +240,7 @@ func TestApisixConsumer_JwtAuth_AsymmetricRS256WithoutAnyKey(t *testing.T) {
 	v := loadApisixConsumerSchema(t)
 	ac := &apisixv2.ApisixConsumer{
 		Spec: apisixv2.ApisixConsumerSpec{
-			AuthParameter: apisixv2.ApisixConsumerAuthParameter{
+			AuthParameter: &apisixv2.ApisixConsumerAuthParameter{
 				JwtAuth: &apisixv2.ApisixConsumerJwtAuth{
 					Value: &apisixv2.ApisixConsumerJwtAuthValue{
 						Key:       "my-key",
@@ -259,7 +259,7 @@ func TestApisixConsumer_JwtAuth_AsymmetricES256WithoutAnyKey(t *testing.T) {
 	v := loadApisixConsumerSchema(t)
 	ac := &apisixv2.ApisixConsumer{
 		Spec: apisixv2.ApisixConsumerSpec{
-			AuthParameter: apisixv2.ApisixConsumerAuthParameter{
+			AuthParameter: &apisixv2.ApisixConsumerAuthParameter{
 				JwtAuth: &apisixv2.ApisixConsumerJwtAuth{
 					Value: &apisixv2.ApisixConsumerJwtAuthValue{
 						Key:       "my-key",
@@ -278,7 +278,7 @@ func TestApisixConsumer_JwtAuth_AsymmetricEdDSAWithoutAnyKey(t *testing.T) {
 	v := loadApisixConsumerSchema(t)
 	ac := &apisixv2.ApisixConsumer{
 		Spec: apisixv2.ApisixConsumerSpec{
-			AuthParameter: apisixv2.ApisixConsumerAuthParameter{
+			AuthParameter: &apisixv2.ApisixConsumerAuthParameter{
 				JwtAuth: &apisixv2.ApisixConsumerJwtAuth{
 					Value: &apisixv2.ApisixConsumerJwtAuthValue{
 						Key:       "my-key",
@@ -297,7 +297,7 @@ func TestApisixConsumer_JwtAuth_AsymmetricWithEmptyPublicKey(t *testing.T) {
 	v := loadApisixConsumerSchema(t)
 	ac := &apisixv2.ApisixConsumer{
 		Spec: apisixv2.ApisixConsumerSpec{
-			AuthParameter: apisixv2.ApisixConsumerAuthParameter{
+			AuthParameter: &apisixv2.ApisixConsumerAuthParameter{
 				JwtAuth: &apisixv2.ApisixConsumerJwtAuth{
 					Value: &apisixv2.ApisixConsumerJwtAuthValue{
 						Key:       "my-key",
@@ -321,7 +321,7 @@ func TestApisixConsumer_JwtAuth_EmptyAlgorithmTreatedAsSymmetric(t *testing.T) {
 	v := loadApisixConsumerSchema(t)
 	ac := &apisixv2.ApisixConsumer{
 		Spec: apisixv2.ApisixConsumerSpec{
-			AuthParameter: apisixv2.ApisixConsumerAuthParameter{
+			AuthParameter: &apisixv2.ApisixConsumerAuthParameter{
 				JwtAuth: &apisixv2.ApisixConsumerJwtAuth{
 					Value: &apisixv2.ApisixConsumerJwtAuthValue{
 						Key:    "my-key",
diff --git a/api/v2/zz_generated.deepcopy.go b/api/v2/zz_generated.deepcopy.go
index 73be4b23..8e659cd9 100644
--- a/api/v2/zz_generated.deepcopy.go
+++ b/api/v2/zz_generated.deepcopy.go
@@ -406,7 +406,18 @@ func (in *ApisixConsumerList) DeepCopyObject() runtime.Object {
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *ApisixConsumerSpec) DeepCopyInto(out *ApisixConsumerSpec) {
 	*out = *in
-	in.AuthParameter.DeepCopyInto(&out.AuthParameter)
+	if in.AuthParameter != nil {
+		in, out := &in.AuthParameter, &out.AuthParameter
+		*out = new(ApisixConsumerAuthParameter)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.Plugins != nil {
+		in, out := &in.Plugins, &out.Plugins
+		*out = make([]ApisixRoutePlugin, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApisixConsumerSpec.
diff --git a/config/crd/bases/apisix.apache.org_apisixconsumers.yaml b/config/crd/bases/apisix.apache.org_apisixconsumers.yaml
index db0ec861..4b004137 100644
--- a/config/crd/bases/apisix.apache.org_apisixconsumers.yaml
+++ b/config/crd/bases/apisix.apache.org_apisixconsumers.yaml
@@ -319,8 +319,32 @@ spec:
                   IngressClassName is the name of an IngressClass cluster resource.
                   The controller uses this field to decide whether the resource should be managed.
                 type: string
-            required:
-            - authParameter
+              plugins:
+                description: |-
+                  Plugins lists additional consumer-scoped plugins to attach to this consumer.
+                  These plugins are applied alongside any authentication plugin derived from AuthParameter.
+                  An enabled plugin with the same name as the auth plugin derived from AuthParameter takes precedence.
+                items:
+                  description: ApisixRoutePlugin represents an APISIX plugin.
+                  properties:
+                    config:
+                      description: Plugin configuration.
+                      x-kubernetes-preserve-unknown-fields: true
+                    enable:
+                      default: true
+                      description: Whether this plugin is in use, default is true.
+                      type: boolean
+                    name:
+                      description: The plugin name.
+                      type: string
+                    secretRef:
+                      description: Plugin configuration secretRef.
+                      type: string
+                  required:
+                  - enable
+                  - name
+                  type: object
+                type: array
             type: object
           status:
             description: ApisixStatus is the status report for Apisix ingress Resources
diff --git a/docs/en/latest/reference/api-reference.md b/docs/en/latest/reference/api-reference.md
index 4ee8a633..ce700a73 100644
--- a/docs/en/latest/reference/api-reference.md
+++ b/docs/en/latest/reference/api-reference.md
@@ -875,6 +875,7 @@ ApisixConsumerSpec defines the desired state of ApisixConsumer.
 | --- | --- |
 | `ingressClassName` _string_ | IngressClassName is the name of an IngressClass cluster resource. The controller uses this field to decide whether the resource should be managed. |
 | `authParameter` _[ApisixConsumerAuthParameter](#apisixconsumerauthparameter)_ | AuthParameter defines the authentication credentials and configuration for this consumer. |
+| `plugins` _[ApisixRoutePlugin](#apisixrouteplugin) array_ | Plugins lists additional consumer-scoped plugins to attach to this consumer. These plugins are applied alongside any authentication plugin derived from AuthParameter. An enabled plugin with the same name as the auth plugin derived from AuthParameter takes precedence. |
 
 
 _Appears in:_
@@ -1163,6 +1164,7 @@ ApisixRoutePlugin represents an APISIX plugin.
 
 
 _Appears in:_
+- [ApisixConsumerSpec](#apisixconsumerspec)
 - [ApisixGlobalRuleSpec](#apisixglobalrulespec)
 - [ApisixPluginConfigSpec](#apisixpluginconfigspec)
 - [ApisixRouteHTTP](#apisixroutehttp)
diff --git a/internal/adc/translator/apisixconsumer.go b/internal/adc/translator/apisixconsumer.go
index 823d65bb..3cae6e08 100644
--- a/internal/adc/translator/apisixconsumer.go
+++ b/internal/adc/translator/apisixconsumer.go
@@ -55,42 +55,54 @@ const (
 func (t *Translator) TranslateApisixConsumer(tctx *provider.TranslateContext, ac *v2.ApisixConsumer) (*TranslateResult, error) {
 	result := &TranslateResult{}
 	plugins := make(adctypes.Plugins)
-	if ac.Spec.AuthParameter.KeyAuth != nil {
-		cfg, err := t.translateConsumerKeyAuthPlugin(tctx, ac.Namespace, ac.Spec.AuthParameter.KeyAuth)
-		if err != nil {
-			return nil, fmt.Errorf("invalid key auth config: %s", err)
+	if ap := ac.Spec.AuthParameter; ap != nil {
+		if ap.KeyAuth != nil {
+			cfg, err := t.translateConsumerKeyAuthPlugin(tctx, ac.Namespace, ap.KeyAuth)
+			if err != nil {
+				return nil, fmt.Errorf("invalid key auth config: %s", err)
+			}
+			plugins["key-auth"] = cfg
+		} else if ap.BasicAuth != nil {
+			cfg, err := t.translateConsumerBasicAuthPlugin(tctx, ac.Namespace, ap.BasicAuth)
+			if err != nil {
+				return nil, fmt.Errorf("invalid basic auth config: %s", err)
+			}
+			plugins["basic-auth"] = cfg
+		} else if ap.JwtAuth != nil {
+			cfg, err := t.translateConsumerJwtAuthPlugin(tctx, ac.Namespace, ap.JwtAuth)
+			if err != nil {
+				return nil, fmt.Errorf("invalid jwt auth config: %s", err)
+			}
+			plugins["jwt-auth"] = cfg
+		} else if ap.WolfRBAC != nil {
+			cfg, err := t.translateConsumerWolfRBACPlugin(tctx, ac.Namespace, ap.WolfRBAC)
+			if err != nil {
+				return nil, fmt.Errorf("invalid wolf rbac config: %s", err)
+			}
+			plugins["wolf-rbac"] = cfg
+		} else if ap.HMACAuth != nil {
+			cfg, err := t.translateConsumerHMACAuthPlugin(tctx, ac.Namespace, ap.HMACAuth)
+			if err != nil {
+				return nil, fmt.Errorf("invalid hmac auth config: %s", err)
+			}
+			plugins["hmac-auth"] = cfg
+		} else if ap.LDAPAuth != nil {
+			cfg, err := t.translateConsumerLDAPAuthPlugin(tctx, ac.Namespace, ap.LDAPAuth)
+			if err != nil {
+				return nil, fmt.Errorf("invalid ldap auth config: %s", err)
+			}
+			plugins["ldap-auth"] = cfg
 		}
-		plugins["key-auth"] = cfg
-	} else if ac.Spec.AuthParameter.BasicAuth != nil {
-		cfg, err := t.translateConsumerBasicAuthPlugin(tctx, ac.Namespace, ac.Spec.AuthParameter.BasicAuth)
-		if err != nil {
-			return nil, fmt.Errorf("invalid basic auth config: %s", err)
-		}
-		plugins["basic-auth"] = cfg
-	} else if ac.Spec.AuthParameter.JwtAuth != nil {
-		cfg, err := t.translateConsumerJwtAuthPlugin(tctx, ac.Namespace, ac.Spec.AuthParameter.JwtAuth)
-		if err != nil {
-			return nil, fmt.Errorf("invalid jwt auth config: %s", err)
-		}
-		plugins["jwt-auth"] = cfg
-	} else if ac.Spec.AuthParameter.WolfRBAC != nil {
-		cfg, err := t.translateConsumerWolfRBACPlugin(tctx, ac.Namespace, ac.Spec.AuthParameter.WolfRBAC)
-		if err != nil {
-			return nil, fmt.Errorf("invalid wolf rbac config: %s", err)
-		}
-		plugins["wolf-rbac"] = cfg
-	} else if ac.Spec.AuthParameter.HMACAuth != nil {
-		cfg, err := t.translateConsumerHMACAuthPlugin(tctx, ac.Namespace, ac.Spec.AuthParameter.HMACAuth)
-		if err != nil {
-			return nil, fmt.Errorf("invalid hmac auth config: %s", err)
-		}
-		plugins["hmac-auth"] = cfg
-	} else if ac.Spec.AuthParameter.LDAPAuth != nil {
-		cfg, err := t.translateConsumerLDAPAuthPlugin(tctx, ac.Namespace, ac.Spec.AuthParameter.LDAPAuth)
-		if err != nil {
-			return nil, fmt.Errorf("invalid ldap auth config: %s", err)
+	}
+
+	// Merge generic consumer-scoped plugins. Only enabled entries are merged;
+	// an enabled plugin with the same name as an auth plugin derived from authParameter takes precedence.
+	for _, plugin := range ac.Spec.Plugins {
+		if !plugin.Enable {
+			continue
 		}
-		plugins["ldap-auth"] = cfg
+		config := t.buildPluginConfig(plugin, ac.Namespace, tctx.Secrets)
+		plugins[plugin.Name] = config
 	}
 
 	username := adctypes.ComposeConsumerName(ac.Namespace, ac.Name)
@@ -107,7 +119,9 @@ func (t *Translator) translateConsumerKeyAuthPlugin(tctx *provider.TranslateCont
 	if cfg.Value != nil {
 		return &adctypes.KeyAuthConsumerConfig{Key: cfg.Value.Key}, nil
 	}
-
+	if cfg.SecretRef == nil {
+		return nil, fmt.Errorf("key-auth: either value or secretRef must be specified")
+	}
 	sec := tctx.Secrets[k8stypes.NamespacedName{
 		Namespace: consumerNamespace,
 		Name:      cfg.SecretRef.Name,
@@ -129,7 +143,9 @@ func (t *Translator) translateConsumerBasicAuthPlugin(tctx *provider.TranslateCo
 			Password: cfg.Value.Password,
 		}, nil
 	}
-
+	if cfg.SecretRef == nil {
+		return nil, fmt.Errorf("basic-auth: either value or secretRef must be specified")
+	}
 	sec := tctx.Secrets[k8stypes.NamespacedName{
 		Namespace: consumerNamespace,
 		Name:      cfg.SecretRef.Name,
@@ -159,6 +175,9 @@ func (t *Translator) translateConsumerWolfRBACPlugin(tctx *provider.TranslateCon
 			HeaderPrefix: cfg.Value.HeaderPrefix,
 		}, nil
 	}
+	if cfg.SecretRef == nil {
+		return nil, fmt.Errorf("wolf-rbac: either value or secretRef must be specified")
+	}
 	sec := tctx.Secrets[k8stypes.NamespacedName{
 		Namespace: consumerNamespace,
 		Name:      cfg.SecretRef.Name,
@@ -194,6 +213,9 @@ func (t *Translator) translateConsumerJwtAuthPlugin(tctx *provider.TranslateCont
 		}, nil
 	}
 
+	if cfg.SecretRef == nil {
+		return nil, fmt.Errorf("jwt-auth: either value or secretRef must be specified")
+	}
 	sec := tctx.Secrets[k8stypes.NamespacedName{
 		Namespace: consumerNamespace,
 		Name:      cfg.SecretRef.Name,
@@ -251,6 +273,9 @@ func (t *Translator) translateConsumerHMACAuthPlugin(tctx *provider.TranslateCon
 		}, nil
 	}
 
+	if cfg.SecretRef == nil {
+		return nil, fmt.Errorf("hmac-auth: either value or secretRef must be specified")
+	}
 	sec := tctx.Secrets[k8stypes.NamespacedName{
 		Namespace: consumerNamespace,
 		Name:      cfg.SecretRef.Name,
@@ -357,6 +382,9 @@ func (t *Translator) translateConsumerLDAPAuthPlugin(tctx *provider.TranslateCon
 		}, nil
 	}
 
+	if cfg.SecretRef == nil {
+		return nil, fmt.Errorf("ldap-auth: either value or secretRef must be specified")
+	}
 	sec := tctx.Secrets[k8stypes.NamespacedName{
 		Namespace: consumerNamespace,
 		Name:      cfg.SecretRef.Name,
diff --git a/internal/controller/apisixconsumer_controller.go b/internal/controller/apisixconsumer_controller.go
index c40345a5..da3594df 100644
--- a/internal/controller/apisixconsumer_controller.go
+++ b/internal/controller/apisixconsumer_controller.go
@@ -184,39 +184,62 @@ func (r *ApisixConsumerReconciler) listApisixConsumerForSecret(ctx context.Conte
 
 func (r *ApisixConsumerReconciler) processSpec(ctx context.Context, tctx *provider.TranslateContext, ac *apiv2.ApisixConsumer) error {
 	var secretRef *corev1.LocalObjectReference
-	if ac.Spec.AuthParameter.KeyAuth != nil {
-		secretRef = ac.Spec.AuthParameter.KeyAuth.SecretRef
-	} else if ac.Spec.AuthParameter.BasicAuth != nil {
-		secretRef = ac.Spec.AuthParameter.BasicAuth.SecretRef
-	} else if ac.Spec.AuthParameter.JwtAuth != nil {
-		secretRef = ac.Spec.AuthParameter.JwtAuth.SecretRef
-	} else if ac.Spec.AuthParameter.WolfRBAC != nil {
-		secretRef = ac.Spec.AuthParameter.WolfRBAC.SecretRef
-	} else if ac.Spec.AuthParameter.HMACAuth != nil {
-		secretRef = ac.Spec.AuthParameter.HMACAuth.SecretRef
-	} else if ac.Spec.AuthParameter.LDAPAuth != nil {
-		secretRef = ac.Spec.AuthParameter.LDAPAuth.SecretRef
-	}
-	if secretRef == nil {
-		return nil
+	if ap := ac.Spec.AuthParameter; ap != nil {
+		if ap.KeyAuth != nil {
+			secretRef = ap.KeyAuth.SecretRef
+		} else if ap.BasicAuth != nil {
+			secretRef = ap.BasicAuth.SecretRef
+		} else if ap.JwtAuth != nil {
+			secretRef = ap.JwtAuth.SecretRef
+		} else if ap.WolfRBAC != nil {
+			secretRef = ap.WolfRBAC.SecretRef
+		} else if ap.HMACAuth != nil {
+			secretRef = ap.HMACAuth.SecretRef
+		} else if ap.LDAPAuth != nil {
+			secretRef = ap.LDAPAuth.SecretRef
+		}
 	}
-
-	namespacedName := types.NamespacedName{
-		Name:      secretRef.Name,
-		Namespace: ac.Namespace,
+	if secretRef != nil && secretRef.Name != "" {
+		namespacedName := types.NamespacedName{
+			Name:      secretRef.Name,
+			Namespace: ac.Namespace,
+		}
+		secret := &corev1.Secret{}
+		if err := r.Get(ctx, namespacedName, secret); err != nil {
+			if k8serrors.IsNotFound(err) {
+				r.Log.Info("secret not found", "secret", namespacedName)
+			} else {
+				r.Log.Error(err, "failed to get secret", "secret", namespacedName)
+				return err
+			}
+		} else {
+			tctx.Secrets[namespacedName] = secret
+		}
 	}
 
-	secret := &corev1.Secret{}
-	if err := r.Get(ctx, namespacedName, secret); err != nil {
-		if k8serrors.IsNotFound(err) {
-			r.Log.Info("secret not found", "secret", namespacedName)
-			return nil
+	for _, plugin := range ac.Spec.Plugins {
+		if !plugin.Enable || plugin.SecretRef == "" {
+			continue
+		}
+		namespacedName := types.NamespacedName{
+			Name:      plugin.SecretRef,
+			Namespace: ac.Namespace,
+		}
+		if _, loaded := tctx.Secrets[namespacedName]; loaded {
+			continue
+		}
+		secret := &corev1.Secret{}
+		if err := r.Get(ctx, namespacedName, secret); err != nil {
+			if k8serrors.IsNotFound(err) {
+				r.Log.Info("secret not found for plugin", "plugin", plugin.Name, "secret", namespacedName)
+			} else {
+				r.Log.Error(err, "failed to get secret for plugin", "plugin", plugin.Name, "secret", namespacedName)
+				return err
+			}
 		} else {
-			r.Log.Error(err, "failed to get secret", "secret", namespacedName)
-			return err
+			tctx.Secrets[namespacedName] = secret
 		}
 	}
-	tctx.Secrets[namespacedName] = secret
 	return nil
 }
 
diff --git a/internal/controller/indexer/indexer.go b/internal/controller/indexer/indexer.go
index 6f0d66a4..5172e487 100644
--- a/internal/controller/indexer/indexer.go
+++ b/internal/controller/indexer/indexer.go
@@ -879,22 +879,29 @@ func ApisixPluginConfigSecretIndexFunc(obj client.Object) (keys []string) {
 func ApisixConsumerSecretIndexFunc(rawObj client.Object) (keys []string) {
 	ac := rawObj.(*apiv2.ApisixConsumer)
 	var secretRef *corev1.LocalObjectReference
-	if ac.Spec.AuthParameter.KeyAuth != nil {
-		secretRef = ac.Spec.AuthParameter.KeyAuth.SecretRef
-	} else if ac.Spec.AuthParameter.BasicAuth != nil {
-		secretRef = ac.Spec.AuthParameter.BasicAuth.SecretRef
-	} else if ac.Spec.AuthParameter.JwtAuth != nil {
-		secretRef = ac.Spec.AuthParameter.JwtAuth.SecretRef
-	} else if ac.Spec.AuthParameter.WolfRBAC != nil {
-		secretRef = ac.Spec.AuthParameter.WolfRBAC.SecretRef
-	} else if ac.Spec.AuthParameter.HMACAuth != nil {
-		secretRef = ac.Spec.AuthParameter.HMACAuth.SecretRef
-	} else if ac.Spec.AuthParameter.LDAPAuth != nil {
-		secretRef = ac.Spec.AuthParameter.LDAPAuth.SecretRef
-	}
-	if secretRef != nil {
+	if ap := ac.Spec.AuthParameter; ap != nil {
+		if ap.KeyAuth != nil {
+			secretRef = ap.KeyAuth.SecretRef
+		} else if ap.BasicAuth != nil {
+			secretRef = ap.BasicAuth.SecretRef
+		} else if ap.JwtAuth != nil {
+			secretRef = ap.JwtAuth.SecretRef
+		} else if ap.WolfRBAC != nil {
+			secretRef = ap.WolfRBAC.SecretRef
+		} else if ap.HMACAuth != nil {
+			secretRef = ap.HMACAuth.SecretRef
+		} else if ap.LDAPAuth != nil {
+			secretRef = ap.LDAPAuth.SecretRef
+		}
+	}
+	if secretRef != nil && secretRef.Name != "" {
 		keys = append(keys, GenIndexKey(ac.GetNamespace(), secretRef.Name))
 	}
+	for _, plugin := range ac.Spec.Plugins {
+		if plugin.Enable && plugin.SecretRef != "" {
+			keys = append(keys, GenIndexKey(ac.GetNamespace(), plugin.SecretRef))
+		}
+	}
 	return
 }
 
diff --git a/internal/webhook/v1/apisixconsumer_webhook.go b/internal/webhook/v1/apisixconsumer_webhook.go
index 796491f5..59212015 100644
--- a/internal/webhook/v1/apisixconsumer_webhook.go
+++ b/internal/webhook/v1/apisixconsumer_webhook.go
@@ -128,24 +128,25 @@ func (v *ApisixConsumerCustomValidator) collectWarnings(ctx context.Context, con
 		})...)
 	}
 
-	params := consumer.Spec.AuthParameter
-	if params.BasicAuth != nil {
-		addSecretWarning(params.BasicAuth.SecretRef)
-	}
-	if params.KeyAuth != nil {
-		addSecretWarning(params.KeyAuth.SecretRef)
-	}
-	if params.WolfRBAC != nil {
-		addSecretWarning(params.WolfRBAC.SecretRef)
-	}
-	if params.JwtAuth != nil {
-		addSecretWarning(params.JwtAuth.SecretRef)
-	}
-	if params.HMACAuth != nil {
-		addSecretWarning(params.HMACAuth.SecretRef)
-	}
-	if params.LDAPAuth != nil {
-		addSecretWarning(params.LDAPAuth.SecretRef)
+	if params := consumer.Spec.AuthParameter; params != nil {
+		if params.BasicAuth != nil {
+			addSecretWarning(params.BasicAuth.SecretRef)
+		}
+		if params.KeyAuth != nil {
+			addSecretWarning(params.KeyAuth.SecretRef)
+		}
+		if params.WolfRBAC != nil {
+			addSecretWarning(params.WolfRBAC.SecretRef)
+		}
+		if params.JwtAuth != nil {
+			addSecretWarning(params.JwtAuth.SecretRef)
+		}
+		if params.HMACAuth != nil {
+			addSecretWarning(params.HMACAuth.SecretRef)
+		}
+		if params.LDAPAuth != nil {
+			addSecretWarning(params.LDAPAuth.SecretRef)
+		}
 	}
 
 	return warnings
diff --git a/internal/webhook/v1/apisixconsumer_webhook_test.go b/internal/webhook/v1/apisixconsumer_webhook_test.go
index e1be420d..b1e0f5c7 100644
--- a/internal/webhook/v1/apisixconsumer_webhook_test.go
+++ b/internal/webhook/v1/apisixconsumer_webhook_test.go
@@ -80,7 +80,7 @@ func TestApisixConsumerValidator_MissingBasicAuthSecret(t *testing.T) {
 		},
 		Spec: apisixv2.ApisixConsumerSpec{
 			IngressClassName: "apisix",
-			AuthParameter: apisixv2.ApisixConsumerAuthParameter{
+			AuthParameter: &apisixv2.ApisixConsumerAuthParameter{
 				BasicAuth: &apisixv2.ApisixConsumerBasicAuth{
 					SecretRef: &corev1.LocalObjectReference{Name: "basic-auth"},
 				},
@@ -104,7 +104,7 @@ func TestApisixConsumerValidator_MultipleSecretWarnings(t *testing.T) {
 		},
 		Spec: apisixv2.ApisixConsumerSpec{
 			IngressClassName: "apisix",
-			AuthParameter: apisixv2.ApisixConsumerAuthParameter{
+			AuthParameter: &apisixv2.ApisixConsumerAuthParameter{
 				BasicAuth: &apisixv2.ApisixConsumerBasicAuth{
 					SecretRef: &corev1.LocalObjectReference{Name: "basic-auth"},
 				},
@@ -144,7 +144,7 @@ func TestApisixConsumerValidator_NoWarningsWhenSecretsExist(t *testing.T) {
 		},
 		Spec: apisixv2.ApisixConsumerSpec{
 			IngressClassName: "apisix",
-			AuthParameter: apisixv2.ApisixConsumerAuthParameter{
+			AuthParameter: &apisixv2.ApisixConsumerAuthParameter{
 				KeyAuth: &apisixv2.ApisixConsumerKeyAuth{
 					SecretRef: &corev1.LocalObjectReference{Name: "key-auth"},
 				},
@@ -181,7 +181,7 @@ func TestApisixConsumerValidator_DeniesOnADCValidationFailure(t *testing.T) {
 		},
 		Spec: apisixv2.ApisixConsumerSpec{
 			IngressClassName: "apisix",
-			AuthParameter: apisixv2.ApisixConsumerAuthParameter{
+			AuthParameter: &apisixv2.ApisixConsumerAuthParameter{
 				KeyAuth: &apisixv2.ApisixConsumerKeyAuth{
 					SecretRef: &corev1.LocalObjectReference{Name: "key-auth"},
 				},
@@ -220,7 +220,7 @@ func TestApisixConsumerValidator_UsesADCValidateEndpointForControlPlane(t *testi
 		},
 		Spec: apisixv2.ApisixConsumerSpec{
 			IngressClassName: managedIngressClassName,
-			AuthParameter: apisixv2.ApisixConsumerAuthParameter{
+			AuthParameter: &apisixv2.ApisixConsumerAuthParameter{
 				KeyAuth: &apisixv2.ApisixConsumerKeyAuth{
 					Value: &apisixv2.ApisixConsumerKeyAuthValue{Key: "shared-key"},
 				},
diff --git a/test/e2e/crds/v2/consumer.go b/test/e2e/crds/v2/consumer.go
index 589e3329..9d646b80 100644
--- a/test/e2e/crds/v2/consumer.go
+++ b/test/e2e/crds/v2/consumer.go
@@ -671,4 +671,190 @@ spec:
 			Eventually(request).WithArguments("/get", "jack", "jackPassword").WithTimeout(5 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusOK))
 		})
 	})
+
+	Context("Test Consumer Plugins - authParameter with extra plugins", func() {
+		// Verify that a consumer with authParameter + plugins (e.g. limit-count) works:
+		// auth is enforced via authParameter and limit-count throttles authenticated traffic.
+		const (
+			consumerWithPlugins = `
+apiVersion: apisix.apache.org/v2
+kind: ApisixConsumer
+metadata:
+  name: consumer-with-plugins
+spec:
+  ingressClassName: %s
+  authParameter:
+    keyAuth:
+      value:
+        key: plugin-test-key
+  plugins:
+  - name: limit-count
+    enable: true
+    config:
+      count: 2
+      time_window: 60
+      rejected_code: 429
+      key: consumer_name
+      policy: local
+`
+			pluginRoute = `
+apiVersion: apisix.apache.org/v2
+kind: ApisixRoute
+metadata:
+  name: plugin-route
+spec:
+  ingressClassName: %s
+  http:
+  - name: rule0
+    match:
+      hosts:
+      - httpbin
+      paths:
+      - /get
+    backends:
+    - serviceName: httpbin-service-e2e-test
+      servicePort: 80
+    authentication:
+      enable: true
+      type: keyAuth
+`
+		)
+
+		It("consumer-level limit-count plugin is enforced", func() {
+			By("apply ApisixRoute")
+			applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "plugin-route"},
+				&apiv2.ApisixRoute{}, fmt.Sprintf(pluginRoute, s.Namespace()))
+
+			By("apply ApisixConsumer with plugins")
+			applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "consumer-with-plugins"},
+				&apiv2.ApisixConsumer{}, fmt.Sprintf(consumerWithPlugins, s.Namespace()))
+
+			By("unauthenticated request is rejected")
+			s.RequestAssert(&scaffold.RequestAssert{
+				Method:  "GET",
+				Path:    "/get",
+				Host:    "httpbin",
+				Headers: map[string]string{"apikey": "wrong-key"},
+				Check:   scaffold.WithExpectedStatus(http.StatusUnauthorized),
+			})
+
+			By("first authenticated request succeeds")
+			s.RequestAssert(&scaffold.RequestAssert{
+				Method:  "GET",
+				Path:    "/get",
+				Host:    "httpbin",
+				Headers: map[string]string{"apikey": "plugin-test-key"},
+				Check:   scaffold.WithExpectedStatus(http.StatusOK),
+			})
+
+			By("second authenticated request succeeds")
+			s.RequestAssert(&scaffold.RequestAssert{
+				Method:  "GET",
+				Path:    "/get",
+				Host:    "httpbin",
+				Headers: map[string]string{"apikey": "plugin-test-key"},
+				Check:   scaffold.WithExpectedStatus(http.StatusOK),
+			})
+
+			By("third request is rate-limited by consumer-level limit-count")
+			s.RequestAssert(&scaffold.RequestAssert{
+				Method:  "GET",
+				Path:    "/get",
+				Host:    "httpbin",
+				Headers: map[string]string{"apikey": "plugin-test-key"},
+				Check:   scaffold.WithExpectedStatus(http.StatusTooManyRequests),
+			})
+
+			By("delete ApisixConsumer")
+			err := s.DeleteResource("ApisixConsumer", "consumer-with-plugins")
+			Expect(err).ShouldNot(HaveOccurred(), "deleting ApisixConsumer")
+
+			By("delete ApisixRoute")
+			err = s.DeleteResource("ApisixRoute", "plugin-route")
+			Expect(err).ShouldNot(HaveOccurred(), "deleting ApisixRoute")
+		})
+	})
+
+	Context("Test Consumer Plugins - plugins only (no authParameter)", func() {
+		// Verify that authParameter can be omitted entirely and auth can be
+		// configured directly via the plugins field.
+		const (
+			consumerPluginsOnly = `
+apiVersion: apisix.apache.org/v2
+kind: ApisixConsumer
+metadata:
+  name: consumer-plugins-only
+spec:
+  ingressClassName: %s
+  plugins:
+  - name: key-auth
+    enable: true
+    config:
+      key: plugins-only-key
+`
+			pluginsOnlyRoute = `
+apiVersion: apisix.apache.org/v2
+kind: ApisixRoute
+metadata:
+  name: plugins-only-route
+spec:
+  ingressClassName: %s
+  http:
+  - name: rule0
+    match:
+      hosts:
+      - httpbin
+      paths:
+      - /get
+    backends:
+    - serviceName: httpbin-service-e2e-test
+      servicePort: 80
+    authentication:
+      enable: true
+      type: keyAuth
+`
+		)
+
+		It("auth plugin configured via plugins field only", func() {
+			By("apply ApisixRoute")
+			applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "plugins-only-route"},
+				&apiv2.ApisixRoute{}, fmt.Sprintf(pluginsOnlyRoute, s.Namespace()))
+
+			By("apply ApisixConsumer with plugins only (no authParameter)")
+			applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "consumer-plugins-only"},
+				&apiv2.ApisixConsumer{}, fmt.Sprintf(consumerPluginsOnly, s.Namespace()))
+
+			By("request with wrong key is rejected")
+			Eventually(func() int {
+				return s.NewAPISIXClient().GET("/get").
+					WithHeader("apikey", "wrong-key").
+					WithHost("httpbin").
+					Expect().Raw().StatusCode
+			}).WithTimeout(10 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusUnauthorized))
+
+			By("request with correct key succeeds")
+			Eventually(func() int {
+				return s.NewAPISIXClient().GET("/get").
+					WithHeader("apikey", "plugins-only-key").
+					WithHost("httpbin").
+					Expect().Raw().StatusCode
+			}).WithTimeout(10 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusOK))
+
+			By("delete ApisixConsumer")
+			err := s.DeleteResource("ApisixConsumer", "consumer-plugins-only")
+			Expect(err).ShouldNot(HaveOccurred(), "deleting ApisixConsumer")
+
+			By("request with correct key is rejected after consumer deletion")
+			Eventually(func() int {
+				return s.NewAPISIXClient().GET("/get").
+					WithHeader("apikey", "plugins-only-key").
+					WithHost("httpbin").
+					Expect().Raw().StatusCode
+			}).WithTimeout(10 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusUnauthorized))
+
+			By("delete ApisixRoute")
+			err = s.DeleteResource("ApisixRoute", "plugins-only-route")
+			Expect(err).ShouldNot(HaveOccurred(), "deleting ApisixRoute")
+		})
+	})
 })