├── .gitignore ├── key-auth-redis ├── hooks.lua ├── daos.lua ├── schema.lua ├── api.lua └── handler.lua └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store -------------------------------------------------------------------------------- /key-auth-redis/hooks.lua: -------------------------------------------------------------------------------- 1 | local events = require "kong.core.events" 2 | local cache = require "kong.tools.database_cache" 3 | 4 | local function invalidate(message_t) 5 | if message_t.collection == "keyauth_credentials" then 6 | cache.delete(cache.keyauth_credential_key(message_t.old_entity and message_t.old_entity.key or message_t.entity.key)) 7 | end 8 | end 9 | 10 | return { 11 | [events.TYPES.ENTITY_UPDATED] = function(message_t) 12 | invalidate(message_t) 13 | end, 14 | [events.TYPES.ENTITY_DELETED] = function(message_t) 15 | invalidate(message_t) 16 | end 17 | } 18 | -------------------------------------------------------------------------------- /key-auth-redis/daos.lua: -------------------------------------------------------------------------------- 1 | local utils = require "kong.tools.utils" 2 | 3 | local function generate_if_missing(v, t, column) 4 | if not v or utils.strip(v) == "" then 5 | return true, nil, {[column] = utils.random_string()} 6 | end 7 | return true 8 | end 9 | 10 | local SCHEMA = { 11 | primary_key = {"id"}, 12 | table = "keyauth_credentials", 13 | fields = { 14 | id = {type = "id", dao_insert_value = true}, 15 | created_at = {type = "timestamp", immutable = true, dao_insert_value = true}, 16 | consumer_id = {type = "id", required = true, foreign = "consumers:id"}, 17 | key = {type = "string", required = false, unique = true, func = generate_if_missing} 18 | }, 19 | marshall_event = function(self, t) 20 | return {id = t.id, consumer_id = t.consumer_id, key = t.key} 21 | end 22 | } 23 | 24 | return {keyauth_credentials = SCHEMA} 25 | -------------------------------------------------------------------------------- /key-auth-redis/schema.lua: -------------------------------------------------------------------------------- 1 | local utils = require "kong.tools.utils" 2 | 3 | 4 | local function check_user(anonymous) 5 | if anonymous == "" or utils.is_valid_uuid(anonymous) then 6 | return true 7 | end 8 | 9 | return false, "the anonymous user must be empty or a valid uuid" 10 | end 11 | 12 | 13 | local function check_keys(keys) 14 | for _, key in ipairs(keys) do 15 | local res, err = utils.validate_header_name(key, false) 16 | 17 | if not res then 18 | return false, "'" .. key .. "' is illegal: " .. err 19 | end 20 | end 21 | 22 | return true 23 | end 24 | 25 | 26 | local function default_key_names(t) 27 | if not t.key_names then 28 | return { "apikey" } 29 | end 30 | end 31 | 32 | 33 | return { 34 | no_consumer = true, 35 | fields = { 36 | key_names = { 37 | required = true, 38 | type = "array", 39 | default = default_key_names, 40 | func = check_keys, 41 | }, 42 | redis_host = { type = "string", default = "" }, 43 | redis_port = { type = "number", default = 6379 }, 44 | redis_password = { type = "string" }, 45 | redis_timeout = { type = "number", default = 2000 }, 46 | redis_connctions = {type = "number", default = 1000}, 47 | fault_tolerant = { type = "boolean", default = true }, 48 | redis_database = { type = "number", default = 0 } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # kong-plugin-auth-redis/ key-auth-redis 2 | 3 | This is a kong plugin tested under 300,000 rps circumstance. 4 | 5 | 十万级别并发请求验证的Kong网关redis权限认证组件 6 | 7 | ## 简介 8 | key-auth with redis 9 | 10 | ## 如何使用 11 | 本插件基于 Kong 10 版本开发. 12 | 使用前请安装Kong 10, 然后把`key-auth-redis` 复制到 **${KongPath}/plugins** 目录下, 13 | 并配置`kong-cluster.conf` (在custome_plugins 中添加key-auth-redis插件, 如要使用限流功能, 请添加general-limiting插件) 14 | 15 | ## 功能 16 | 1. 可连接单个redis; 17 | 2. 根据redis查询得到key(token); 18 | 3. 保存key到kong本地数据库;(取消) 19 | 20 | ### 备选功能 21 | 1. 为每一个consumer创建一个rate-limiting插件以限制用户的访问速率; 22 | 23 | ## 使用步骤 24 | 1. 通过`kong api`或者`kong dashboard`,为指定API注册插件`key-auth-redis` 25 | 2. 设置相应的配置 26 | 27 | | 名称 | 类型 | 默认值 | 说明 | 28 | | ----------- | --------- | --- | --- | 29 | | key_names | string | function |自定义api_key的名称(一般设为token)| 30 | | hide_credentials | boolean |false |一个可选的布尔值,指示插件将凭据隐藏到上游API服务器。在代理请求之前,它将被Kong删除。| 31 | | anonymous | string | \`\` |如果身份验证失败,则可以使用可选的字符串(消费者uuid)值作为“匿名”消费者。如果为空(默认),则请求将失败并发送身份验证失败4xx | 32 | | redis_host | string | \`\` |redis服务器IP地址(必须)| 33 | | redis_port | number | 6379 |redis服务器端口| 34 | | redis_password | string | \`\` |redis密码| 35 | | redis_timeout | number | 2000 |redis超时时间(ms)| 36 | | rate_limiting | boolean | false |指定是否在创建consumer的同时,创建与其相关的rate_limiting插件| 37 | | apiname_uri_lastest | boolean | false |指定rate-limiting插件所作用的api_name为consumer访问的uri的最后一段字符串。例如uri是`/key/auth/redis`,则api\_name=redis| 38 | | limit_by | string | consumer |根据类型限制访问速率,类型有`consumer`, `credential`, `ip`| 39 | | policy | string | cluster |根据策略配置rate-limiting计数器,策略类型有`local`, `cluster`, `redis`| 40 | | fault_tolerant | boolean | true |用于确定请求是否应被代理,即使Kong连接第三方数据存储时遇到问题。如果真正的请求将被代理,无论如何有效地禁用速率限制功能,直到数据存储再次工作。如果为false,那么客户端将看到500错误。| 41 | | redis_database | number | 0 |redis数据库个数,当只有一个数据库时,只需采用默认值0| 42 | | second | number | 无 |从第一次访问算起,一秒限制的次数| 43 | | minute | number | 无 |从第一次访问算起,一分钟限制的次数| 44 | | hour | number | 无 |从第一次访问算起,一小时限制的次数| 45 | | day | number | 无 |从第一次访问算起,一天内限制的次数| 46 | | month | number | 无 |从第一次访问算起,一个月内限制的次数| 47 | | year | number | 无 |从第一次访问算起,一年内限制的次数| 48 | 49 | 3. 配置设置成功后,启用插件便能够生效。 50 | 51 | -------------------------------------------------------------------------------- /key-auth-redis/api.lua: -------------------------------------------------------------------------------- 1 | local crud = require "kong.api.crud_helpers" 2 | local redis = require "resty.redis" 3 | local cache = require "kong.tools.database_cache" 4 | local responses = require "kong.tools.responses" 5 | local singletons = require "kong.singletons" 6 | 7 | return { 8 | ["/api/loadCache"] = { 9 | POST = function(self, helpers) 10 | -- ngx.log(ngx.INFO, "kong.lua loadCaches()") 11 | -- ngx.log(ngx.INFO, "self.perload_redis_host: ", self.params.perload_redis_host) 12 | -- ngx.log(ngx.INFO, "self.perload_redis_pwd: ", self.params.perload_redis_pwd) 13 | -- 连接redis 14 | local cache_red = redis:new() 15 | local ok, err = cache_red:connect(self.params.perload_redis_host, 6379) 16 | 17 | if err then 18 | ngx.log(ngx.ERR, "LoadCaches() failed to connect to Redis: ", err) 19 | return responses.HTTP_INTERNAL_SERVER_ERROR("Failed to connect to perload Redis.") 20 | end 21 | 22 | local ok, err = cache_red:auth(self.params.perload_redis_pwd) 23 | if err then 24 | ngx.log(ngx.ERR, "LoadCaches() failed to login to Redis: ", err) 25 | return responses.HTTP_INTERNAL_SERVER_ERROR("Failed to login to perload Redis.") 26 | end 27 | 28 | local preload_cache = cache_red:keys("*") 29 | -- 遍历 30 | for k, v in pairs(preload_cache) do 31 | cache.sh_set(v, 1, 31536000) 32 | end 33 | 34 | local keepalive_ok, err = cache_red:set_keepalive(300000, 100) 35 | if not keepalive_ok then 36 | ngx.log(ngx.ERR, "failed to set keepalive: ", err) 37 | end 38 | return responses.send_HTTP_OK("get perload data.") 39 | end 40 | }, 41 | ["/consumers/:username_or_id/key-auth/"] = { 42 | before = function(self, dao_factory, helpers) 43 | crud.find_consumer_by_username_or_id(self, dao_factory, helpers) 44 | self.params.consumer_id = self.consumer.id 45 | end, 46 | 47 | GET = function(self, dao_factory) 48 | crud.paginated_set(self, dao_factory.keyauth_credentials) 49 | end, 50 | 51 | PUT = function(self, dao_factory) 52 | crud.put(self.params, dao_factory.keyauth_credentials) 53 | end, 54 | 55 | POST = function(self, dao_factory) 56 | crud.post(self.params, dao_factory.keyauth_credentials) 57 | end 58 | }, 59 | ["/consumers/:username_or_id/key-auth/:credential_key_or_id"] = { 60 | before = function(self, dao_factory, helpers) 61 | crud.find_consumer_by_username_or_id(self, dao_factory, helpers) 62 | self.params.consumer_id = self.consumer.id 63 | 64 | local credentials, err = crud.find_by_id_or_field( 65 | dao_factory.keyauth_credentials, 66 | { consumer_id = self.params.consumer_id }, 67 | self.params.credential_key_or_id, 68 | "key" 69 | ) 70 | 71 | if err then 72 | return helpers.yield_error(err) 73 | elseif next(credentials) == nil then 74 | return helpers.responses.send_HTTP_NOT_FOUND() 75 | end 76 | self.params.credential_key_or_id = nil 77 | 78 | self.keyauth_credential = credentials[1] 79 | end, 80 | 81 | GET = function(self, dao_factory, helpers) 82 | return helpers.responses.send_HTTP_OK(self.keyauth_credential) 83 | end, 84 | 85 | PATCH = function(self, dao_factory) 86 | crud.patch(self.params, dao_factory.keyauth_credentials, self.keyauth_credential) 87 | end, 88 | 89 | DELETE = function(self, dao_factory) 90 | crud.delete(self.keyauth_credential, dao_factory.keyauth_credentials) 91 | end 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /key-auth-redis/handler.lua: -------------------------------------------------------------------------------- 1 | local cache = require "kong.tools.database_cache" 2 | local responses = require "kong.tools.responses" 3 | local constants = require "kong.constants" 4 | local singletons = require "kong.singletons" 5 | local BasePlugin = require "kong.plugins.base_plugin" 6 | 7 | local ngx_set_header = ngx.req.set_header 8 | local ngx_get_headers = ngx.req.get_headers 9 | local set_uri_args = ngx.req.set_uri_args 10 | local get_uri_args = ngx.req.get_uri_args 11 | local clear_header = ngx.req.clear_header 12 | local type = type 13 | 14 | local _realm = 'Key realm="'.._KONG._NAME..'"' 15 | -- 引入redis模块 16 | local redis = require "resty.redis" 17 | -- 引入crud模块 18 | local crud = require "kong.api.crud_helpers" 19 | -- reports utils 20 | local reports = require "kong.core.reports" 21 | local utils = require "kong.tools.utils" 22 | 23 | local KeyAuthHandler = BasePlugin:extend() 24 | 25 | KeyAuthHandler.PRIORITY = 1000 26 | 27 | function KeyAuthHandler:new() 28 | KeyAuthHandler.super.new(self, "key-auth-redis") 29 | end 30 | 31 | -- 连接redis方法 32 | local function connect_to_redis(conf) 33 | local red = redis:new() 34 | red:set_timeout(conf.redis_timeout) 35 | 36 | local ok, err = red:connect(conf.redis_host, conf.redis_port) 37 | if err then 38 | return nil, err 39 | end 40 | 41 | if conf.redis_password and conf.redis_password ~= "" then 42 | local ok, err = red:auth(conf.redis_password) 43 | if err then 44 | return nil, err 45 | end 46 | end 47 | 48 | return red 49 | end 50 | 51 | local function do_authentication(conf) 52 | if type(conf.key_names) ~= "table" then 53 | ngx.log(ngx.ERR, "[key-auth-redis] no conf.key_names set, aborting plugin execution") 54 | return false, {status = 500, message= "Invalid plugin configuration"} 55 | end 56 | 57 | local key 58 | local headers = ngx_get_headers() 59 | local uri_args = get_uri_args() 60 | 61 | -- search in headers & querystring 62 | for i = 1, #conf.key_names do 63 | local name = conf.key_names[i] 64 | local v = headers[name] 65 | if not v then 66 | -- search in querystring 67 | v = uri_args[name] 68 | end 69 | 70 | if type(v) == "string" then 71 | key = v 72 | if conf.hide_credentials then 73 | uri_args[name] = nil 74 | set_uri_args(uri_args) 75 | clear_header(name) 76 | end 77 | break 78 | elseif type(v) == "table" then 79 | -- duplicate API key, HTTP 401 多个相同的API key请求参数 80 | return false, {status = 401, message = "Duplicate API key found"} 81 | end 82 | end 83 | 84 | -- this request is missing an API key, HTTP 401 85 | if not key then 86 | ngx.header["WWW-Authenticate"] = _realm 87 | return false, {status = 401, message = "No API key found in headers" 88 | .." or querystring"} 89 | end 90 | 91 | -- 先查预热缓存 92 | local credential, err = cache.sh_get(key) 93 | if err then 94 | return false, {status = 500, message = tostring(err)} 95 | end 96 | -- ngx.log(ngx.INFO, "预热缓存: ", credential) 97 | 98 | -- 如果没有,则查询redis 99 | if not credential then 100 | -- 连接redis 101 | local red, err = connect_to_redis(conf) 102 | if err then 103 | ngx.log(ngx.CRIT, "failed to connect to Redis: ", err) 104 | return false, {status = 500, message= "Failed to connect to Redis."} 105 | end 106 | 107 | -- 查询redis 108 | local cred, err = red:get(key) 109 | if not cred or cred == ngx.null or cred ~= uri_args["token"] then 110 | return false, {status = 403, message = "Invalid authentication credentials"} 111 | else 112 | -- ngx.log(ngx.INFO, "redis缓存: ", cred) 113 | -- 存cache 114 | -- cache.sh_set(key, 1, 31536000) 115 | -- ngx.log(ngx.INFO, "存cache: ", cache.get(key)) 116 | -- redis连接池 最大空闲时间5分钟 117 | local keepalive_ok, err = red:set_keepalive(300000, conf.redis_connctions) 118 | if not keepalive_ok then 119 | ngx.log(ngx.CRIT, "failed to set keepalive: ", err) 120 | end 121 | end 122 | end 123 | return true 124 | end 125 | 126 | function KeyAuthHandler:access(conf) 127 | KeyAuthHandler.super.access(self) 128 | 129 | local ok, err = do_authentication(conf) 130 | if not ok then 131 | return responses.send(err.status, err.message) 132 | end 133 | 134 | end 135 | 136 | return KeyAuthHandler 137 | --------------------------------------------------------------------------------