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