mirror of
https://github.com/alibaba/higress.git
synced 2026-05-26 05:37:25 +08:00
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:
@@ -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的名称 |
|
| `name` | string | 必填 | - | 配置该consumer的名称 |
|
||||||
|
|
||||||
### 鉴权配置(非必需)
|
### 鉴权配置(非必需)
|
||||||
@@ -45,7 +46,7 @@ description: Key 认证插件配置参考
|
|||||||
|
|
||||||
### 全局配置认证和路由粒度进行鉴权
|
### 全局配置认证和路由粒度进行鉴权
|
||||||
|
|
||||||
以下配置将对网关特定路由或域名开启Key Auth认证和鉴权。credential字段不能重复。
|
以下配置将对网关特定路由或域名开启Key Auth认证和鉴权。credential 或 credentials 中的访问凭证不能重复。
|
||||||
|
|
||||||
在实例级别做如下插件配置:
|
在实例级别做如下插件配置:
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ Plugin Execution Priority: `310`
|
|||||||
The configuration field descriptions for each item in `consumers` are as follows:
|
The configuration field descriptions for each item in `consumers` are as follows:
|
||||||
| Name | Data Type | Requirements | Default Value | Description |
|
| 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. |
|
| `name` | string | Required | - | Configures the name for this consumer. |
|
||||||
|
|
||||||
### Authorization Configuration (Optional)
|
### Authorization Configuration (Optional)
|
||||||
@@ -37,7 +38,7 @@ The configuration field descriptions for each item in `consumers` are as follows
|
|||||||
|
|
||||||
## Configuration Example
|
## Configuration Example
|
||||||
### Global Configuration for Authentication and Granular Route Authorization
|
### 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:
|
At the instance level, do the following plugin configuration:
|
||||||
```yaml
|
```yaml
|
||||||
|
|||||||
@@ -55,6 +55,13 @@ type Consumer struct {
|
|||||||
// @Description en-US The credential of the consumer.
|
// @Description en-US The credential of the consumer.
|
||||||
// @Scope GLOBAL
|
// @Scope GLOBAL
|
||||||
Credential string `yaml:"credential"`
|
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
|
// @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")
|
return errors.New("consumer name is required")
|
||||||
}
|
}
|
||||||
credential := item.Get("credential")
|
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")
|
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{
|
consumer := Consumer{
|
||||||
Name: name.String(),
|
Name: name.String(),
|
||||||
Credential: credential.String(),
|
|
||||||
}
|
}
|
||||||
|
if credential.Exists() {
|
||||||
|
consumer.Credential = credential.String()
|
||||||
|
} else {
|
||||||
|
consumer.Credentials = consumerCredentials
|
||||||
|
}
|
||||||
|
|
||||||
global.consumers = append(global.consumers, consumer)
|
global.consumers = append(global.consumers, consumer)
|
||||||
global.credential2Name[credential.String()] = name.String()
|
for _, credential := range consumerCredentials {
|
||||||
|
global.credential2Name[credential] = name.String()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,27 @@ var basicKeyAuthConfig = func() json.RawMessage {
|
|||||||
return data
|
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 {
|
var globalAuthFalseConfig = func() json.RawMessage {
|
||||||
data, _ := json.Marshal(map[string]interface{}{
|
data, _ := json.Marshal(map[string]interface{}{
|
||||||
@@ -187,6 +208,62 @@ var invalidDuplicateCredentialConfig = func() json.RawMessage {
|
|||||||
return data
|
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 列表
|
// 测试配置:规则配置 - 带 allow 列表
|
||||||
var ruleConfig = func() json.RawMessage {
|
var ruleConfig = func() json.RawMessage {
|
||||||
data, _ := json.Marshal(map[string]interface{}{
|
data, _ := json.Marshal(map[string]interface{}{
|
||||||
@@ -259,6 +336,24 @@ func TestParseGlobalConfig(t *testing.T) {
|
|||||||
require.False(t, keyAuthConfig.InQuery)
|
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) {
|
t.Run("global auth false config", func(t *testing.T) {
|
||||||
host, status := test.NewTestHost(globalAuthFalseConfig)
|
host, status := test.NewTestHost(globalAuthFalseConfig)
|
||||||
@@ -355,6 +450,27 @@ func TestParseGlobalConfig(t *testing.T) {
|
|||||||
defer host.Reset()
|
defer host.Reset()
|
||||||
require.Equal(t, types.OnPluginStartStatusFailed, status)
|
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()
|
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
|
// 测试全局认证开启 - 无效的 API key
|
||||||
t.Run("global auth true - invalid api key", func(t *testing.T) {
|
t.Run("global auth true - invalid api key", func(t *testing.T) {
|
||||||
host, status := test.NewTestHost(basicKeyAuthConfig)
|
host, status := test.NewTestHost(basicKeyAuthConfig)
|
||||||
|
|||||||
Reference in New Issue
Block a user