diff --git a/plugins/wasm-go/extensions/key-auth/README.md b/plugins/wasm-go/extensions/key-auth/README.md index 786124e0..dff0d87a 100644 --- a/plugins/wasm-go/extensions/key-auth/README.md +++ b/plugins/wasm-go/extensions/key-auth/README.md @@ -32,7 +32,8 @@ description: Key 认证插件配置参考 | 名称 | 数据类型 | 填写要求 | 默认值 | 描述 | | ------------ | -------- | -------- | ------ | ------------------------ | -| `credential` | string | 必填 | - | 配置该consumer的访问凭证 | +| `credential` | string | `credential` 和 `credentials` 二选一 | - | 配置该consumer的一个访问凭证 | +| `credentials` | array of string | `credential` 和 `credentials` 二选一 | - | 配置该consumer的多个访问凭证,不能与 `credential` 同时配置 | | `name` | string | 必填 | - | 配置该consumer的名称 | ### 鉴权配置(非必需) @@ -45,7 +46,7 @@ description: Key 认证插件配置参考 ### 全局配置认证和路由粒度进行鉴权 -以下配置将对网关特定路由或域名开启Key Auth认证和鉴权。credential字段不能重复。 +以下配置将对网关特定路由或域名开启Key Auth认证和鉴权。credential 或 credentials 中的访问凭证不能重复。 在实例级别做如下插件配置: diff --git a/plugins/wasm-go/extensions/key-auth/README_EN.md b/plugins/wasm-go/extensions/key-auth/README_EN.md index 3abccc4f..bb23535d 100644 --- a/plugins/wasm-go/extensions/key-auth/README_EN.md +++ b/plugins/wasm-go/extensions/key-auth/README_EN.md @@ -27,7 +27,8 @@ Plugin Execution Priority: `310` The configuration field descriptions for each item in `consumers` are as follows: | Name | Data Type | Requirements | Default Value | Description | | ------------ | --------- | ------------ | ------------- | ------------------------------ | -| `credential` | string | Required | - | Configures the access credential for this consumer. | +| `credential` | string | Either `credential` or `credentials` must be configured | - | Configures one access credential for this consumer. | +| `credentials` | array of string | Either `credential` or `credentials` must be configured | - | Configures multiple access credentials for this consumer. Cannot be used together with `credential`. | | `name` | string | Required | - | Configures the name for this consumer. | ### Authorization Configuration (Optional) @@ -37,7 +38,7 @@ The configuration field descriptions for each item in `consumers` are as follows ## Configuration Example ### Global Configuration for Authentication and Granular Route Authorization -The following configuration will enable Key Auth authentication and authorization for specific routes or hostnames in the gateway. The `credential` field must not repeat. +The following configuration will enable Key Auth authentication and authorization for specific routes or hostnames in the gateway. The `credential` or `credentials` values must not repeat. At the instance level, do the following plugin configuration: ```yaml diff --git a/plugins/wasm-go/extensions/key-auth/main.go b/plugins/wasm-go/extensions/key-auth/main.go index 6d9aa33f..72c6d90c 100644 --- a/plugins/wasm-go/extensions/key-auth/main.go +++ b/plugins/wasm-go/extensions/key-auth/main.go @@ -55,6 +55,13 @@ type Consumer struct { // @Description en-US The credential of the consumer. // @Scope GLOBAL Credential string `yaml:"credential"` + + // @Title 访问凭证列表 + // @Title en-US Credentials + // @Description 该调用方的访问凭证列表,不能与 credential 同时配置。 + // @Description en-US The credentials of the consumer. Cannot be configured together with credential. + // @Scope GLOBAL + Credentials []string `yaml:"credentials"` } // @Name key-auth @@ -186,19 +193,51 @@ func parseGlobalConfig(json gjson.Result, global *KeyAuthConfig, log log.Log) er return errors.New("consumer name is required") } credential := item.Get("credential") - if !credential.Exists() || credential.String() == "" { + credentials := item.Get("credentials") + if credential.Exists() && credentials.Exists() { + return errors.New("'credential' and 'credentials' can't appear at the same time") + } + if !credential.Exists() && !credentials.Exists() { return errors.New("consumer credential is required") } - if _, ok := global.credential2Name[credential.String()]; ok { - return errors.New("duplicate consumer credential: " + credential.String()) + + consumerCredentials := make([]string, 0, 1) + if credential.Exists() { + if credential.String() == "" { + return errors.New("consumer credential is required") + } + consumerCredentials = append(consumerCredentials, credential.String()) + } else { + if !credentials.IsArray() || len(credentials.Array()) == 0 { + return errors.New("consumer credentials cannot be empty") + } + for _, credential := range credentials.Array() { + if credential.String() == "" { + return errors.New("consumer credential is required") + } + consumerCredentials = append(consumerCredentials, credential.String()) + } + } + + for _, credential := range consumerCredentials { + if _, ok := global.credential2Name[credential]; ok { + return errors.New("duplicate consumer credential: " + credential) + } } consumer := Consumer{ - Name: name.String(), - Credential: credential.String(), + Name: name.String(), } + if credential.Exists() { + consumer.Credential = credential.String() + } else { + consumer.Credentials = consumerCredentials + } + global.consumers = append(global.consumers, consumer) - global.credential2Name[credential.String()] = name.String() + for _, credential := range consumerCredentials { + global.credential2Name[credential] = name.String() + } } return nil } diff --git a/plugins/wasm-go/extensions/key-auth/main_test.go b/plugins/wasm-go/extensions/key-auth/main_test.go index 8bb0880b..20be672e 100644 --- a/plugins/wasm-go/extensions/key-auth/main_test.go +++ b/plugins/wasm-go/extensions/key-auth/main_test.go @@ -44,6 +44,27 @@ var basicKeyAuthConfig = func() json.RawMessage { return data }() +// 测试配置:consumer 使用多个 credentials +var pluralCredentialsConfig = func() json.RawMessage { + data, _ := json.Marshal(map[string]interface{}{ + "consumers": []map[string]interface{}{ + { + "name": "consumer1", + "credentials": []string{"token1", "token1-alt"}, + }, + { + "name": "consumer2", + "credential": "token2", + }, + }, + "keys": []string{"x-api-key", "apikey"}, + "in_header": true, + "in_query": false, + "global_auth": true, + }) + return data +}() + // 测试配置:全局认证关闭 var globalAuthFalseConfig = func() json.RawMessage { data, _ := json.Marshal(map[string]interface{}{ @@ -187,6 +208,62 @@ var invalidDuplicateCredentialConfig = func() json.RawMessage { return data }() +// 测试配置:无效配置 - credentials 中的重复 credential +var invalidDuplicatePluralCredentialConfig = func() json.RawMessage { + data, _ := json.Marshal(map[string]interface{}{ + "consumers": []map[string]interface{}{ + { + "name": "consumer1", + "credential": "token1", + }, + { + "name": "consumer2", + "credentials": []string{"token1"}, + }, + }, + "keys": []string{"x-api-key"}, + "in_header": true, + "in_query": false, + "global_auth": true, + }) + return data +}() + +// 测试配置:无效配置 - credential 和 credentials 同时配置 +var invalidMixedCredentialFormsConfig = func() json.RawMessage { + data, _ := json.Marshal(map[string]interface{}{ + "consumers": []map[string]interface{}{ + { + "name": "consumer1", + "credential": "token1", + "credentials": []string{"token1-alt"}, + }, + }, + "keys": []string{"x-api-key"}, + "in_header": true, + "in_query": false, + "global_auth": true, + }) + return data +}() + +// 测试配置:无效配置 - 空的 credentials +var invalidEmptyPluralCredentialsConfig = func() json.RawMessage { + data, _ := json.Marshal(map[string]interface{}{ + "consumers": []map[string]interface{}{ + { + "name": "consumer1", + "credentials": []string{}, + }, + }, + "keys": []string{"x-api-key"}, + "in_header": true, + "in_query": false, + "global_auth": true, + }) + return data +}() + // 测试配置:规则配置 - 带 allow 列表 var ruleConfig = func() json.RawMessage { data, _ := json.Marshal(map[string]interface{}{ @@ -259,6 +336,24 @@ func TestParseGlobalConfig(t *testing.T) { require.False(t, keyAuthConfig.InQuery) }) + // 测试 consumer credentials 数组配置解析 + t.Run("plural credentials config", func(t *testing.T) { + host, status := test.NewTestHost(pluralCredentialsConfig) + defer host.Reset() + require.Equal(t, types.OnPluginStartStatusOK, status) + + config, err := host.GetMatchConfig() + require.NoError(t, err) + require.NotNil(t, config) + + keyAuthConfig := config.(*KeyAuthConfig) + require.Len(t, keyAuthConfig.consumers, 2) + require.Equal(t, []string{"token1", "token1-alt"}, keyAuthConfig.consumers[0].Credentials) + require.Equal(t, "consumer1", keyAuthConfig.credential2Name["token1"]) + require.Equal(t, "consumer1", keyAuthConfig.credential2Name["token1-alt"]) + require.Equal(t, "consumer2", keyAuthConfig.credential2Name["token2"]) + }) + // 测试全局认证关闭配置 t.Run("global auth false config", func(t *testing.T) { host, status := test.NewTestHost(globalAuthFalseConfig) @@ -355,6 +450,27 @@ func TestParseGlobalConfig(t *testing.T) { defer host.Reset() require.Equal(t, types.OnPluginStartStatusFailed, status) }) + + // 测试无效配置 - credentials 中的重复 credential + t.Run("invalid duplicate plural credential config", func(t *testing.T) { + host, status := test.NewTestHost(invalidDuplicatePluralCredentialConfig) + defer host.Reset() + require.Equal(t, types.OnPluginStartStatusFailed, status) + }) + + // 测试无效配置 - credential 和 credentials 互斥 + t.Run("invalid mixed credential forms config", func(t *testing.T) { + host, status := test.NewTestHost(invalidMixedCredentialFormsConfig) + defer host.Reset() + require.Equal(t, types.OnPluginStartStatusFailed, status) + }) + + // 测试无效配置 - 空的 credentials + t.Run("invalid empty plural credentials config", func(t *testing.T) { + host, status := test.NewTestHost(invalidEmptyPluralCredentialsConfig) + defer host.Reset() + require.Equal(t, types.OnPluginStartStatusFailed, status) + }) }) } @@ -421,6 +537,38 @@ func TestOnHTTPRequestHeaders(t *testing.T) { host.CompleteHttp() }) + // 测试全局认证开启 - credentials 数组中的 API key + t.Run("global auth true - valid plural credentials api key", func(t *testing.T) { + host, status := test.NewTestHost(pluralCredentialsConfig) + defer host.Reset() + require.Equal(t, types.OnPluginStartStatusOK, status) + + action := host.CallOnHttpRequestHeaders([][2]string{ + {":authority", "example.com"}, + {":path", "/test"}, + {":method", "GET"}, + {"x-api-key", "token1-alt"}, + }) + + require.Equal(t, types.ActionContinue, action) + require.Equal(t, types.ActionContinue, host.GetHttpStreamAction()) + + localResponse := host.GetLocalResponse() + require.Nil(t, localResponse, "Valid API key from credentials should pass through") + + headers := host.GetRequestHeaders() + consumerHeaderFound := false + for _, header := range headers { + if header[0] == "x-mse-consumer" && header[1] == "consumer1" { + consumerHeaderFound = true + break + } + } + require.True(t, consumerHeaderFound, "X-Mse-Consumer header should be added") + + host.CompleteHttp() + }) + // 测试全局认证开启 - 无效的 API key t.Run("global auth true - invalid api key", func(t *testing.T) { host, status := test.NewTestHost(basicKeyAuthConfig)