feature: support plural key-auth credentials (#3849)

Signed-off-by: GHX5T-SOL <200635707+GHX5T-SOL@users.noreply.github.com>
Co-authored-by: GHX5T-SOL <200635707+GHX5T-SOL@users.noreply.github.com>
This commit is contained in:
Ghxst
2026-05-24 11:56:58 +02:00
committed by GitHub
parent b99e0c5027
commit 8a0f8a8208
4 changed files with 199 additions and 10 deletions

View File

@@ -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 中的访问凭证不能重复。
在实例级别做如下插件配置:

View File

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

View File

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

View File

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