diff --git a/apisix/plugins/limit-count.lua b/apisix/plugins/limit-count.lua
index d2831c36a281..1a52ef4e73b8 100644
--- a/apisix/plugins/limit-count.lua
+++ b/apisix/plugins/limit-count.lua
@@ -36,11 +36,10 @@ local schema = {
properties = {
count = {type = "integer", exclusiveMinimum = 0},
time_window = {type = "integer", exclusiveMinimum = 0},
- key = {
- type = "string",
- enum = {"remote_addr", "server_addr", "http_x_real_ip",
- "http_x_forwarded_for", "consumer_name", "service_id"},
- default = "remote_addr",
+ key = {type = "string", default = "remote_addr"},
+ key_type = {type = "string",
+ enum = {"var", "var_combination"},
+ default = "var",
},
rejected_code = {
type = "integer", minimum = 200, maximum = 599, default = 503
@@ -171,7 +170,29 @@ function _M.access(conf, ctx)
return 500
end
- local key = (ctx.var[conf.key] or "") .. ctx.conf_type .. ctx.conf_version
+ local conf_key = conf.key
+ local key
+ if conf.key_type == "var_combination" then
+ local err, n_resolved
+ key, err, n_resolved = core.utils.resolve_var(conf_key, ctx.var);
+ if err then
+ core.log.error("could not resolve vars in ", conf_key, " error: ", err)
+ end
+
+ if n_resolved == 0 then
+ key = nil
+ end
+ else
+ key = ctx.var[conf_key]
+ end
+
+ if key == nil then
+ core.log.info("bypass the limit count as the key is empty")
+ -- Bypass the limit count when the key is empty.
+ -- This behavior is the same as Nginx
+ return
+ end
+ key = key .. ctx.conf_type .. ctx.conf_version
core.log.info("limit key: ", key)
local delay, remaining = lim:incoming(key, true)
diff --git a/docs/en/latest/plugins/limit-count.md b/docs/en/latest/plugins/limit-count.md
index 2a4302e61a41..2c267603f0bf 100644
--- a/docs/en/latest/plugins/limit-count.md
+++ b/docs/en/latest/plugins/limit-count.md
@@ -39,7 +39,8 @@ Limit request rate by a fixed number of requests in a given time window.
| ------------------- | ------- | --------------------------------------- | ------------- | ------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| count | integer | required | | count > 0 | the specified number of requests threshold. |
| time_window | integer | required | | time_window > 0 | the time window in seconds before the request count is reset. |
-| key | string | optional | "remote_addr" | ["remote_addr", "server_addr", "http_x_real_ip", "http_x_forwarded_for", "consumer_name", "service_id"] | The user specified key to limit the count.
Now accept those as key: "remote_addr"(client's IP), "server_addr"(server's IP), "X-Forwarded-For/X-Real-IP" in request header, "consumer_name"(consumer's username) and "service_id". |
+| key_type | string | optional | "var" | ["var", "var_combination"] | the type of key. |
+| key | string | optional | "remote_addr" | | the user specified key to limit the rate. If the `key_type` is "var", the key will be treated as a name of variable. If the `key_type` is "var_combination", the key will be a combination of variables. For example, if we use "$remote_addr $consumer_name" as keys, plugin will be restricted by two keys which are "remote_addr" and "consumer_name". |
| rejected_code | integer | optional | 503 | [200,...,599] | The HTTP status code returned when the request exceeds the threshold is rejected, default 503. |
| rejected_msg | string | optional | | non-empty | The response body returned when the request exceeds the threshold is rejected. |
| policy | string | optional | "local" | ["local", "redis", "redis-cluster"] | The rate-limiting policies to use for retrieving and incrementing the limits. Available values are `local`(the counters will be stored locally in-memory on the node), `redis`(counters are stored on a Redis server and will be shared across the nodes, usually use it to do the global speed limit), and `redis-cluster` which works the same as `redis` but with redis cluster. |
@@ -53,11 +54,9 @@ Limit request rate by a fixed number of requests in a given time window.
| redis_cluster_nodes | array | required when policy is `redis-cluster` | | | When using `redis-cluster` policy,This property is a list of addresses of Redis cluster service nodes (at least two). |
| redis_cluster_name | string | required when policy is `redis-cluster` | | | When using `redis-cluster` policy, this property is the name of Redis cluster service nodes. |
-**Key can be customized by the user, only need to modify a line of code of the plug-in to complete. It is a security consideration that is not open in the plugin.**
-
## How To Enable
-Here's an example, enable the `limit count` plugin on the specified route:
+Here's an example, enable the `limit count` plugin on the specified route when setting `key_type` to `var` :
```shell
curl -i http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
@@ -68,13 +67,38 @@ curl -i http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335
"count": 2,
"time_window": 60,
"rejected_code": 503,
+ "key_type": "var",
"key": "remote_addr"
}
},
"upstream": {
"type": "roundrobin",
"nodes": {
- "39.97.63.215:80": 1
+ "127.0.0.1:9001": 1
+ }
+ }
+}'
+```
+
+Here's an example, enable the `limit count` plugin on the specified route when setting `key_type` to `var_combination` :
+
+```shell
+curl -i http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
+{
+ "uri": "/index.html",
+ "plugins": {
+ "limit-count": {
+ "count": 2,
+ "time_window": 60,
+ "rejected_code": 503,
+ "key_type": "var_combination",
+ "key": "$consumer_name $remote_addr"
+ }
+ },
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": {
+ "127.0.0.1:9001": 1
}
}
}'
diff --git a/docs/zh/latest/plugins/limit-count.md b/docs/zh/latest/plugins/limit-count.md
index d8e505c94556..51173642d1eb 100644
--- a/docs/zh/latest/plugins/limit-count.md
+++ b/docs/zh/latest/plugins/limit-count.md
@@ -42,7 +42,8 @@ title: limit-count
| ------------------- | ------- | --------------------------------- | ------------- | ------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| count | integer | 必须 | | count > 0 | 指定时间窗口内的请求数量阈值 |
| time_window | integer | 必须 | | time_window > 0 | 时间窗口的大小(以秒为单位),超过这个时间就会重置 |
-| key | string | 可选 | "remote_addr" | ["remote_addr", "server_addr", "http_x_real_ip", "http_x_forwarded_for", "consumer_name", "service_id"] | 用来做请求计数的有效值。
例如,可以使用主机名(或服务器区域)作为关键字,以便限制每个主机名规定时间内的请求次数。我们也可以使用客户端地址作为关键字,这样我们就可以避免单个客户端规定时间内多次的连接我们的服务。
当前接受的 key 有:"remote_addr"(客户端 IP 地址), "server_addr"(服务端 IP 地址), 请求头中的"X-Forwarded-For" 或 "X-Real-IP", "consumer_name"(consumer 的 username), "service_id" 。 |
+| key_type | string | 可选 | "var" | ["var", "var_combination"] | key 的类型 |
+| key | string | 可选 | "remote_addr" | | 用来做请求计数的依据。如果 `key_type` 为 "var",那么 key 会被当作变量名称。如果 `key_type` 为 "var_combination",那么 key 会当作变量组。比如如果设置 "$remote_addr $consumer_name" 作为 keys,那么插件会同时受 remote_addr 和 consumer_name 两个 key 的约束。 |
| rejected_code | integer | 可选 | 503 | [200,...,599] | 当请求超过阈值被拒绝时,返回的 HTTP 状态码 |
| rejected_msg | string | 可选 | | 非空 | 当请求超过阈值被拒绝时,返回的响应体。 |
| policy | string | 可选 | "local" | ["local", "redis", "redis-cluster"] | 用于检索和增加限制的速率限制策略。可选的值有:`local`(计数器被以内存方式保存在节点本地,默认选项) 和 `redis`(计数器保存在 Redis 服务节点上,从而可以跨节点共享结果,通常用它来完成全局限速);以及`redis-cluster`,跟 redis 功能一样,只是使用 redis 集群方式。 |
@@ -56,13 +57,11 @@ title: limit-count
| redis_cluster_nodes | array | 当 policy 为 `redis-cluster` 时必填| | | 当使用 `redis-cluster` 限速策略时,该属性是 Redis 集群服务节点的地址列表(至少需要两个地址)。 |
| redis_cluster_name | string | 当 policy 为 `redis-cluster` 时必填 | | | 当使用 `redis-cluster` 限速策略时,该属性是 Redis 集群服务节点的名称。 |
-**key 是可以被用户自定义的,只需要修改插件的一行代码即可完成。并没有在插件中放开是处于安全的考虑。**
-
## 如何使用
### 开启插件
-下面是一个示例,在指定的 `route` 上开启了 `limit count` 插件:
+下面是一个示例,在指定的 `route` 上开启了 `limit count` 插件,并设置 `key_type` 为 `var`:
```shell
curl -i http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
@@ -85,6 +84,30 @@ curl -i http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335
}'
```
+下面是一个示例,在指定的 `route` 上开启了 `limit count` 插件,并设置 `key_type` 为 `var_combination`:
+
+```shell
+curl -i http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
+{
+ "uri": "/index.html",
+ "plugins": {
+ "limit-count": {
+ "count": 2,
+ "time_window": 60,
+ "rejected_code": 503,
+ "key_type": "var_combination",
+ "key": "$consumer_name $remote_addr"
+ }
+ },
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": {
+ "127.0.0.1:9001": 1
+ }
+ }
+}'
+```
+
你也可以通过 web 界面来完成上面的操作,先增加一个 route,然后在插件页面中添加 limit-count 插件:
![添加插件](../../../assets/images/plugin/limit-count-1.png)
diff --git a/t/control/services.t b/t/control/services.t
index 734afcc2b067..c702a7ceb0cd 100644
--- a/t/control/services.t
+++ b/t/control/services.t
@@ -155,7 +155,7 @@ services:
}
}
--- response_body
-{"id":"5","plugins":{"limit-count":{"allow_degradation":false,"count":2,"key":"remote_addr","policy":"local","rejected_code":503,"show_limit_quota_header":true,"time_window":60}},"upstream":{"hash_on":"vars","nodes":[{"host":"127.0.0.1","port":1980,"weight":1}],"pass_host":"pass","scheme":"http","type":"roundrobin"}}
+{"id":"5","plugins":{"limit-count":{"allow_degradation":false,"count":2,"key":"remote_addr","key_type":"var","policy":"local","rejected_code":503,"show_limit_quota_header":true,"time_window":60}},"upstream":{"hash_on":"vars","nodes":[{"host":"127.0.0.1","port":1980,"weight":1}],"pass_host":"pass","scheme":"http","type":"roundrobin"}}
diff --git a/t/plugin/limit-count-redis-cluster.t b/t/plugin/limit-count-redis-cluster.t
index 0eb2d4818b74..7b2b04f5bb87 100644
--- a/t/plugin/limit-count-redis-cluster.t
+++ b/t/plugin/limit-count-redis-cluster.t
@@ -238,7 +238,7 @@ unlock with key route#1#redis-cluster
"limit-count": {
"count": 9999,
"time_window": 60,
- "key": "http_x_real_ip",
+ "key": "remote_addr",
"policy": "redis-cluster",
"redis_cluster_nodes": [
"127.0.0.1:5000",
@@ -328,7 +328,7 @@ code: 200
"limit-count": {
"count": ]] .. count .. [[,
"time_window": 60,
- "key": "http_x_real_ip",
+ "key": "remote_addr",
"policy": "redis-cluster",
"redis_cluster_nodes": [
"127.0.0.1:5000",
@@ -393,7 +393,7 @@ code: 503
"limit-count": {
"count": 9999,
"time_window": 60,
- "key": "http_x_real_ip",
+ "key": "remote_addr",
"policy": "redis-cluster",
"allow_degradation": true,
"redis_cluster_nodes": [
diff --git a/t/plugin/limit-count.t b/t/plugin/limit-count.t
index 298dbcf3c952..b04642a6d755 100644
--- a/t/plugin/limit-count.t
+++ b/t/plugin/limit-count.t
@@ -56,12 +56,12 @@ done
-=== TEST 2: wrong value of key
+=== TEST 2: set key empty
--- config
location /t {
content_by_lua_block {
local plugin = require("apisix.plugins.limit-count")
- local ok, err = plugin.check_schema({count = 2, time_window = 60, rejected_code = 503, key = 'host'})
+ local ok, err = plugin.check_schema({count = 2, time_window = 60, rejected_code = 503})
if not ok then
ngx.say(err)
end
@@ -72,7 +72,6 @@ done
--- request
GET /t
--- response_body
-property "key" validation failed: matches none of the enum values
done
--- no_error_log
[error]
diff --git a/t/plugin/limit-count2.t b/t/plugin/limit-count2.t
index e3a2aa034269..016fb2909cea 100644
--- a/t/plugin/limit-count2.t
+++ b/t/plugin/limit-count2.t
@@ -178,3 +178,239 @@ GET /hello
--- error_code: 503
--- response_body
{"error_msg":"Requests are too frequent, please try again later."}
+
+
+
+=== TEST 6: update route, use new limit configuration
+--- config
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ local code, body = t('/apisix/admin/routes/1',
+ ngx.HTTP_PUT,
+ [[{
+ "plugins": {
+ "limit-count": {
+ "count": 2,
+ "time_window": 60,
+ "rejected_code": 503,
+ "key": "http_a",
+ "key_type": "var"
+ }
+ },
+ "upstream": {
+ "nodes": {
+ "127.0.0.1:1980": 1
+ },
+ "type": "roundrobin"
+ },
+ "uri": "/hello"
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- request
+GET /t
+--- response_body
+passed
+--- no_error_log
+[error]
+
+
+
+=== TEST 7: exceed the burst when key_type is var
+--- config
+ location /t {
+ content_by_lua_block {
+ local json = require "t.toolkit.json"
+ local http = require "resty.http"
+ local uri = "http://127.0.0.1:" .. ngx.var.server_port
+ .. "/hello"
+ local ress = {}
+ for i = 1, 4 do
+ local httpc = http.new()
+ local res, err = httpc:request_uri(uri, {headers = {a = 1}})
+ if not res then
+ ngx.say(err)
+ return
+ end
+ table.insert(ress, res.status)
+ end
+ ngx.say(json.encode(ress))
+ }
+ }
+--- request
+GET /t
+--- no_error_log
+[error]
+--- response_body
+[200,200,503,503]
+
+
+
+=== TEST 8: bypass empty key when key_type is var
+--- config
+ location /t {
+ content_by_lua_block {
+ local json = require "t.toolkit.json"
+ local http = require "resty.http"
+ local uri = "http://127.0.0.1:" .. ngx.var.server_port
+ .. "/hello"
+ local ress = {}
+ for i = 1, 4 do
+ local httpc = http.new()
+ local res, err = httpc:request_uri(uri)
+ if not res then
+ ngx.say(err)
+ return
+ end
+ table.insert(ress, res.status)
+ end
+ ngx.say(json.encode(ress))
+ }
+ }
+--- request
+GET /t
+--- no_error_log
+[error]
+--- response_body
+[200,200,200,200]
+
+
+
+=== TEST 9: update route, set key type to var_combination
+--- config
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ local code, body = t('/apisix/admin/routes/1',
+ ngx.HTTP_PUT,
+ [[{
+ "plugins": {
+ "limit-count": {
+ "count": 2,
+ "time_window": 60,
+ "rejected_code": 503,
+ "key": "$http_a $http_b",
+ "key_type": "var_combination"
+ }
+ },
+ "upstream": {
+ "nodes": {
+ "127.0.0.1:1980": 1
+ },
+ "type": "roundrobin"
+ },
+ "uri": "/hello"
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- request
+GET /t
+--- response_body
+passed
+--- no_error_log
+[error]
+
+
+
+=== TEST 10: exceed the burst when key_type is var_combination
+--- config
+ location /t {
+ content_by_lua_block {
+ local json = require "t.toolkit.json"
+ local http = require "resty.http"
+ local uri = "http://127.0.0.1:" .. ngx.var.server_port
+ .. "/hello"
+ local ress = {}
+ for i = 1, 4 do
+ local httpc = http.new()
+ local res, err = httpc:request_uri(uri, {headers = {a = 1}})
+ if not res then
+ ngx.say(err)
+ return
+ end
+ table.insert(ress, res.status)
+ end
+ ngx.say(json.encode(ress))
+ }
+ }
+--- request
+GET /t
+--- no_error_log
+[error]
+--- response_body
+[200,200,503,503]
+
+
+
+=== TEST 11: don`t exceed the burst when key_type is var_combination
+--- config
+ location /t {
+ content_by_lua_block {
+ local json = require "t.toolkit.json"
+ local http = require "resty.http"
+ local uri = "http://127.0.0.1:" .. ngx.var.server_port
+ .. "/hello"
+ local ress = {}
+ for i = 1, 2 do
+ local httpc = http.new()
+ local res, err = httpc:request_uri(uri, {headers = {a = i}})
+ if not res then
+ ngx.say(err)
+ return
+ end
+ table.insert(ress, res.status)
+ end
+ ngx.say(json.encode(ress))
+ }
+ }
+--- request
+GET /t
+--- no_error_log
+[error]
+--- response_body
+[503,200]
+
+
+
+=== TEST 12: bypass empty key when key_type is var_combination
+--- config
+ location /t {
+ content_by_lua_block {
+ local json = require "t.toolkit.json"
+ local http = require "resty.http"
+ local uri = "http://127.0.0.1:" .. ngx.var.server_port
+ .. "/hello"
+ local ress = {}
+ for i = 1, 2 do
+ local httpc = http.new()
+ local res, err = httpc:request_uri(uri)
+ if not res then
+ ngx.say(err)
+ return
+ end
+ table.insert(ress, res.status)
+ end
+ ngx.say(json.encode(ress))
+ }
+ }
+--- request
+GET /t
+--- no_error_log
+[error]
+--- response_body
+[200,200]
+--- error_log
+bypass the limit count as the key is empty