Mind and Hand Help

Redis Rules Operator

-- Access Control Rules Operator -- 提供对规则组和规则的增删改查操作 -- 规则组存储在 Redis 中的结构: -- access_control: -- rules_groups: -- group_id: -- md5: string 规则组的 MD5 摘要 -- last_updated: string timestamp -- parents: set 包含的其他规则组 -- children: set 被哪些规则组包含 -- rules_map:hash 规则 ID 到规则 JSON 的映射 local _M = {} local redis_utils = require("lua_modules.redis.redis_utils") local config = require("lua_modules.redis.access_control.key_config") local date_utils = require("lua_modules.commons.date_utils") local json_utils = require("lua_modules.commons.json_utils") local cjson = require("cjson.safe") -- 检查指定的规则组是否存在 -- @param group_id 规则组 ID -- @param red 已连接的 Redis 实例 -- @return: md5 摘要字符串 或 nil + 错误信息 local function has_group(group_id, red) local md5_key = config.get_md5_key(group_id) local md5_value, err = red:get(md5_key) if md5_value == nil then ngx.log(ngx.ERR, "Failed to get md5 for group_id ", group_id, ": ", tostring(err)) return nil, "LOST_CONNECTION" end if md5_value == ngx.null or md5_value == "" then return nil, "Invalid md5 value" end return md5_value end -- 计算并返回指定规则组的 MD5 摘要 计算的核心过程会删除不存在的 parents -- @param group_id 规则组 ID -- @param red 已连接的 Redis 实例 -- @return 成功时返回 MD5 摘要字符串,失败时返回 nil 和错误信息 local function calculate_group_md5(group_id, red) -- 获取规则组的规则 ID 列表 参与 MD5 计算 local function get_rule_ids() -- 获取 rule_ids 用于计算 MD5 local rules_map_key = config.get_rules_map_key(group_id) -- 获取map中所有的规则ID local rule_ids, err = red:hkeys(rules_map_key) if not rule_ids then ngx.log(ngx.ERR, "Failed to get rule IDs for group_id ", group_id, ": ", err) rule_ids = {} end return rule_ids end -- 获取包含的其他规则组的 MD5 列表 参与 MD5 计算 local function get_parents_md5_list() -- 获取依赖的其他规则组的 MD5 列表 local parents_md5_list = {} local parents_set_key = config.get_parents_set_key(group_id) local included_groups, err = red:smembers(parents_set_key) if not included_groups then ngx.log(ngx.ERR, "Failed to get included groups for group_id ", group_id, ": ", err) included_groups = {} else -- 遍历map,获取每个包含的规则组的 MD5 for _, included_group_id in ipairs(included_groups) do local included_md5, err = has_group(included_group_id, red) if included_md5 then table.insert(parents_md5_list, included_md5) else -- 这个包含的规则组不存在,删除它 ngx.log(ngx.ERR, "Included group_id ", included_group_id, " does not exist, removing from parents of group_id ", group_id, ": ", err) -- 删除不存在的包含关系 !!! local ok, err = red:srem(parents_set_key, included_group_id) if not ok then ngx.log( ngx.ERR, "Failed to remove non-existent included group_id ", included_group_id, " from includes of group_id ", group_id, ": ", err ) end end end end return parents_md5_list end -- 用于存储需要参与计算 MD5 的数据 -- 参与计算的数据包括:group_id、所有的规则id(如果有)、包含的其他规则组的(如果有) md5(排序) local data = {} table.insert(data, group_id) local rule_ids = get_rule_ids() if #rule_ids > 0 then table.sort(rule_ids) -- 确保顺序一致 for _, id in ipairs(rule_ids) do table.insert(data, id) end end local parents_md5_list = get_parents_md5_list() if #parents_md5_list > 0 then table.sort(parents_md5_list) -- 确保顺序一致 for _, parent_md5 in ipairs(parents_md5_list) do table.insert(data, parent_md5) end end -- 计算 MD5 摘要 local concat_str = table.concat(data, "|") local md5 = ngx.md5(concat_str) -- ngx.md5 返回的是 32 位的十六进制字符串 return md5 end -- 刷新规则组的MD5摘要 递归向下刷新所有受影响的规则组 -- @param group_id 规则组 ID -- @param red 已连接的 Redis 实例 local function refresh_md5(group_id, red, refreshed) refreshed = refreshed or {} local old_md5, err = has_group(group_id, red) if old_md5 then local new_md5 = calculate_group_md5(group_id, red) -- 计算的过程中会删除不存在的 parents if new_md5 == old_md5 then return old_md5 -- 没有变化 end -- 一旦自身的 MD5 变化,所有依赖它的规则组都需要刷新 local md5_key = config.get_md5_key(group_id) local last_updated_key = config.get_last_updated_key(group_id) red:set(md5_key, new_md5) red:set(last_updated_key, date_utils.now_str()) local children_set_key = config.get_children_set_key(group_id) local children, err = red:smembers(children_set_key) if not children then ngx.log(ngx.ERR, "Failed to get children groups for group_id ", group_id, ": ", tostring(err)) children = {} end -- 标记已经刷新过,避免重复刷新 refreshed[group_id] = true if #children > 0 then for _, child_group_id in ipairs(children) do if not refreshed[child_group_id] then ngx.log(ngx.DEBUG, "Refreshing MD5 for affected group_id: ", child_group_id) refresh_md5(child_group_id, red, refreshed) end end end return new_md5 else return nil, "group does not exist" end end -- 创建一个空的规则组 -- @param group_id 规则组 ID -- @param red 已连接的 Redis 实例 -- @return 成功时返回 true,失败时返回 nil 和错误信息 local function create_empty_group(group_id, red) if not group_id or type(group_id) ~= "string" or group_id == "" then return nil, "Invalid group_id" end -- 获取元数据的各个键 Redis 的哲学:存在的才占空间。 local last_updated_key = config.get_last_updated_key(group_id) -- 初始化元数据 red:set(last_updated_key, date_utils.now_str()) -- 计算并设置 MD5 摘要 local md5_key = config.get_md5_key(group_id) local md5_value = calculate_group_md5(group_id, red) red:set(md5_key, md5_value) return md5_value end -- 获取所有规则组 ID 列表 -- @param red 已连接的 Redis 实例 -- @return 成功时返回规则组 ID 到信息的映射表,失败时返回 nil 和错误信息 function _M.get_all_groups(red) local groups_info = {} local pattern = config.get_prefix() .. ":*:md5" local md5_keys, err = redis_utils.scan_keys(red, pattern, 100) if not md5_keys then return nil, "failed to scan md5 keys: " .. tostring(err) end for _, md5_key in ipairs(md5_keys) do local group_id = md5_key:match("^" .. config.get_prefix() .. ":(.+):md5$") local md5 = has_group(group_id, red) if md5 then local parents_set_key = config.get_parents_set_key(group_id) local parents = red:smembers(parents_set_key) or {} local children_set_key = config.get_children_set_key(group_id) local children = red:smembers(children_set_key) or {} groups_info[group_id] = { ["_group_id"] = group_id, ["_parents"] = parents, ["_children"] = children, ["_md5"] = md5, } end end return groups_info, nil end -- 模块方法:新建一个规则组 -- @param group_id 规则组 ID -- @param red 已连接的 Redis 实例 -- @return 成功时返回 true,失败时返回 nil 和错误信息 function _M.add_group(group_id, red) -- 如果规则组已经存在,直接返回成功 local md5_value, err = has_group(group_id, red) if md5_value then return nil, "Group already exists|digest=" .. md5_value end return create_empty_group(group_id, red) end -- 模块方法:删除指定的规则组及其所有相关数据 -- @param group_id 规则组 ID -- @param red 已连接的 Redis 实例 -- @return 成功时返回 true,失败时返回 nil 和错误信息 function _M.remove_group(group_id, red) -- 检查规则组是否存在 local md5_value, err = has_group(group_id, red) if not md5_value then return nil, "Group does not exist: " .. tostring(err) end -- 获取相关的 Redis 键 local keys = config.get_group_key_list(group_id) -- 获取被哪些规则组包含 local children_set_key = config.get_children_set_key(group_id) local children, err = red:smembers(children_set_key) if not children then children = {} end -- 获取包含的其他规则组 local parents_set_key = config.get_parents_set_key(group_id) local parents, err = red:smembers(parents_set_key) if not parents then parents = {} end -- 删除规则组的所有相关键 for _, key in pairs(keys) do red:del(key) end if #children > 0 then -- 刷新所有受影响的规则组的 MD5 摘要 for _, child_group_id in ipairs(children) do refresh_md5(child_group_id, red) -- 计算的过程中会删除不存在的 parents end end if #parents > 0 then -- 删除包含自身的规则组中的包含关系 for _, parent_group_id in ipairs(parents) do local parent_children_set_key = config.get_children_set_key(parent_group_id) red:srem(parent_children_set_key, group_id) end end return true, nil end -- 环路检测 -- @param from string 起始规则组 ID -- @param target string 目标规则组 ID -- @param red 已连接的 Redis 实例 local function loop_detection(from, target, red) -- 使用深度优先搜索检测环路 local function dfs(curr_group_id, target_group_id, visited) if curr_group_id == target_group_id then return true -- 找到环路 end if visited[curr_group_id] then return false -- 已访问过,避免重复 end visited[curr_group_id] = true local parents_set_key = config.get_parents_set_key(curr_group_id) local parents, err = red:smembers(parents_set_key) -- 获取所有的包含的规则组 if not parents then return false -- 获取失败,假设没有环路 end for _, parent_group_id in ipairs(parents) do if dfs(parent_group_id, target_group_id, visited) then return true end end return false end -- 从 from 开始,检查是否能回到 target return dfs(from, target, {}) end -- 模块方法:添加包含关系 -- @param group_id 规则组 ID -- @param parent_group_id 要包含的规则组 ID -- @param red 已连接的 Redis 实例 function _M.add_parent(group_id, parent_group_id, red) -- 检查规则组是否存在 local md5_value, err = has_group(group_id, red) if not md5_value then return nil, "Group does not exist: " .. tostring(err) end -- 检查要包含的规则组是否存在 local parent_md5_value, err = has_group(parent_group_id, red) if not parent_md5_value then return nil, "Parent group does not exist: " .. tostring(err) end -- 如果 parent_group_id 已经直接或间接包含了 group_id,则添加会导致环路 local loop = loop_detection(parent_group_id, group_id, red) if loop then return nil, "Adding this include would create a loop" end local parents_set_key = config.get_parents_set_key(group_id) local children_set_key = config.get_children_set_key(parent_group_id) -- 检查是否已经包含,避免重复添加 local exists, err = red:sismember(parents_set_key, parent_group_id) if exists == nil then return nil, "failed to check membership: " .. tostring(err) end if exists ~= 0 then return true, nil end -- 添加包含关系 (双向) red:sadd(parents_set_key, parent_group_id) red:sadd(children_set_key, group_id) -- 刷新规则组的 MD5 摘要 refresh_md5(group_id, red) -- 计算的过程中会删除不存在的 parents return true, nil end -- 模块方法:删除包含关系 -- @param group_id 规则组 ID -- @param parent_group_id 要删除包含的规则组 ID -- @param red 已连接的 Redis 实例 function _M.remove_parent(group_id, parent_group_id, red) -- 检查规则组是否存在 local md5_value, err = has_group(group_id, red) if not md5_value then return nil, "Group does not exist: " .. tostring(err) end -- 检查要包含的规则组是否存在,可能被手动删了 local parent_md5_value, err = has_group(parent_group_id, red) if not parent_md5_value then ngx.log(ngx.WARN, "Include group does not exist: ", tostring(err)) end -- 获取相关的 Redis 键 local parents_set_key = config.get_parents_set_key(group_id) local children_set_key = config.get_children_set_key(parent_group_id) ngx.log(ngx.ERR, "Deleting include: ", group_id, " -> ", parent_group_id) -- 删除包含关系 (双向) red:srem(parents_set_key, parent_group_id) red:srem(children_set_key, group_id) -- 刷新规则组的 MD5 摘要 refresh_md5(group_id, red) -- 计算的过程中会删除不存在的 parents return true, nil end -- 模块方法:获取规则组的包含列表 (提供这个方法,由上层决定是否递归获取所有包含的规则组) -- @param group_id 规则组 ID -- @param red 已连接的 Redis 实例 -- @return 成功时返回包含的规则组 ID 数组,失败时返回 nil 和错误信息 function _M.get_parents(group_id, red) if not group_id or type(group_id) ~= "string" or group_id == "" then return nil, "Invalid group_id" end -- 检查规则组是否存在 local md5_value, err = has_group(group_id, red) if not md5_value then return nil, "Group does not exist: " .. tostring(err) end -- 获取规则组的包含列表 key local parents_set_key = config.get_parents_set_key(group_id) -- 获取所有包含的规则组 ID local parents, err = red:smembers(parents_set_key) if not parents then return nil, "failed to get included groups: " .. tostring(err) end return parents, nil end -- 模块方法:获取规则组的所有规则(不递归获取所有包含的规则组) -- @param group_id 规则组 ID -- @param red 已连接的 Redis 实例 -- @return 成功时返回规则数组,失败时返回 nil 和错误信息 function _M.get_rules(group_id, red) -- 检查规则组是否存在 local md5_value, err = has_group(group_id, red) if not md5_value then return nil, "Group does not exist: " .. tostring(err) end -- 获取规则组的规则 map key local rules_map_key = config.get_rules_map_key(group_id) -- 获取所有规则 local rules_map, err = red:hgetall(rules_map_key) if not rules_map then return nil, "failed to get rules map: " .. tostring(err) end local rules = {} for i = 1, #rules_map, 2 do local rule_id = rules_map[i] local rule_json = rules_map[i + 1] if rule_json and rule_json ~= ngx.null then local rule, err = cjson.decode(rule_json) if rule then table.insert(rules, rule) else ngx.log(ngx.ERR, "Failed to decode rule JSON for rule_id ", rule_id, ": ", err) end end end -- 这里取消排序,由上层决定是否排序 -- if #rules > 0 then -- -- 按优先级排序,优先级高的在前面 -- table.sort(rules, function(a, b) -- return (a.priority or 0) > (b.priority or 0) -- end) -- end return rules, nil end -- 模块方法:添加规则 -- @param group_id 规则组 ID -- @param rule 规则对象(Lua 表) -- @param rule_id 规则 ID -- @param red -- @return 成功时返回规则的 JSON 字符串,失败时返回 nil 和错误信息 function _M.add_rule(group_id, rule, rule_id, red) rule._group_id = group_id -- 为规则添加元信息 rule._id = rule_id -- 为规则添加元信息 if rule.priority == nil or type(rule.priority) ~= "number" then rule.priority = 1 -- 默认优先级为 1 end -- 序列化规则,存储为 JSON 字符串 -- 这里不检查 rule 的内容,由调用方负责 local rule_json, err = cjson.encode(rule) if not rule_json then return nil, "failed to encode rule to JSON: " .. tostring(err) end -- 如果规则组不存在,先创建一个空的规则组 if not has_group(group_id, red) then local ok, err = create_empty_group(group_id, red) if not ok then return nil, "failed to create empty group: " .. tostring(err) end end -- 获取相关的 Redis 键 local rules_map_key = config.get_rules_map_key(group_id) local last_updated_key = config.get_last_updated_key(group_id) -- 检查规则 ID 是否已存在 local exists, err = red:hexists(rules_map_key, rule_id) if exists == nil then return nil, "failed to check rule existence: " .. tostring(err) end -- 规则 ID 已存在 检测内容是否相同 if exists ~= 0 then -- 获取规则id对应的规则 json 对比rule_json local existing_rule_json, err = red:hget(rules_map_key, rule_id) if existing_rule_json then if json_utils.deep_equal(existing_rule_json, rule_json) then -- 规则相同,不做任何操作 return rule_json, nil end else ngx.log(ngx.ERR, "Failed to get existing rule JSON for rule_id ", rule_id, ": ", tostring(err)) -- 继续往下走,覆盖旧规则 end -- 规则不同,继续往下走,覆盖旧规则 end -- 添加规则到 Redis red:hset(rules_map_key, rule_id, rule_json) red:set(last_updated_key, date_utils.now_str()) -- 刷新规则组的 MD5 摘要 refresh_md5(group_id, red) return rule_json, nil end -- 模块方法:获取指定规则组中的指定规则 -- @param group_id 规则组 ID -- @param rule_id 规则 ID -- @param red 已连接的 Redis 实例 -- @return 成功时返回规则对象(Lua 表),失败时返回 nil 和错误信息 function _M.get_rule(group_id, rule_id, red) -- 检查规则组是否存在 local md5_value, err = has_group(group_id, red) if not md5_value then return nil, "Group does not exist: " .. tostring(err) end -- 获取规则组的规则 map key local rules_map_key = config.get_rules_map_key(group_id) local rule_json, err = red:hget(rules_map_key, rule_id) -- 获取指定规则 if not rule_json then return nil, "failed to get rule: " .. tostring(err) end if rule_json == ngx.null or rule_json == "" then return nil, "rule not found" end local rule, err = cjson.decode(rule_json) if not rule then return nil, "failed to decode rule JSON: " .. tostring(err) end return rule, nil end -- 模块方法:删除指定规则组中的指定规则 -- @param group_id 规则组 ID -- @param rule_id 规则 ID -- @param red 已连接的 Redis 实例 -- @return 成功时返回 true,失败时返回 nil 和错误信息 function _M.remove_rule(group_id, rule_id, red) -- 检查规则组是否存在 local md5_value, err = has_group(group_id, red) if not md5_value then return nil, "Group does not exist: " .. tostring(err) end -- 获取相关的 Redis 键 local rules_map_key = config.get_rules_map_key(group_id) local last_updated_key = config.get_last_updated_key(group_id) -- 删除规则 red:hdel(rules_map_key, rule_id) red:set(last_updated_key, date_utils.now_str()) -- 刷新规则组的 MD5 摘要 refresh_md5(group_id, red) return true, nil end -- -------------------- 特殊的操作(上层应用模板不提供的操作) -------------------- -- 模块方法:获取规则组的 MD5 摘要 目的:检测规则组是否变化 -- @param group_id 规则组 ID -- @param red 已连接的 Redis 实例 function _M.get_md5(group_id, red) if not group_id or type(group_id) ~= "string" or group_id == "" then return nil, "Invalid group_id" end -- 检查规则组是否存在 local md5_value, err = has_group(group_id, red) if not md5_value then return nil, "Group does not exist: " .. tostring(err) end return md5_value, nil end -- 模块方法:刷新规则组的 MD5 摘要 (关键:计算的时候会更新includes中不存在的规则组) -- @param group_id 规则组 ID function _M.refresh_md5(group_id, red) local new_md5, err = refresh_md5(group_id, red) if not new_md5 then ngx.log(ngx.ERR, "Failed to refresh md5 for group_id ", group_id, ": ", tostring(err)) end return new_md5, err end return _M
27 January 2026