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