MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Redis脚本管理命令实现的权限控制

2021-08-308.0k 阅读

Redis脚本管理基础

Redis 是一个开源的内存数据结构存储系统,广泛应用于缓存、消息队列、分布式锁等场景。Redis 提供了脚本管理功能,允许用户通过 Lua 脚本来执行复杂的操作。

EVAL 命令

EVAL 命令用于在 Redis 中执行 Lua 脚本。其基本语法如下:

EVAL script numkeys key [key ...] arg [arg ...]
  • script:是 Lua 脚本代码,以字符串形式传递。
  • numkeys:表示在脚本中使用的键名参数的数量。
  • key [key ...]:是在脚本中会用到的 Redis 键名。
  • arg [arg ...]:是传递给脚本的附加参数。

例如,以下 Lua 脚本用于获取一个键的值并返回:

local key = KEYS[1]
return redis.call('GET', key)

在 Redis 客户端中执行该脚本可以这样写:

EVAL "local key = KEYS[1] return redis.call('GET', key)" 1 mykey

这里 1 表示使用了 1 个键,mykey 就是对应的键名。

EVALSHA 命令

EVALSHA 命令与 EVAL 类似,但它执行的是通过 SCRIPT LOAD 命令预先加载到 Redis 服务器的脚本。语法如下:

EVALSHA sha1 numkeys key [key ...] arg [arg ...]
  • sha1:是通过 SCRIPT LOAD 命令返回的脚本 SHA1 摘要。

先使用 SCRIPT LOAD 加载脚本:

SCRIPT LOAD "local key = KEYS[1] return redis.call('GET', key)"

假设返回的 SHA1 摘要为 abcdef1234567890,那么可以使用 EVALSHA 执行:

EVALSHA abcdef1234567890 1 mykey

这样做的好处是,对于重复执行的脚本,只需要加载一次,减少网络传输开销。

权限控制的必要性

在多用户或多应用场景下,对 Redis 脚本执行进行权限控制是非常必要的。

安全性考虑

如果不进行权限控制,任何用户都可以执行任意 Redis 脚本,这可能导致数据泄露、数据损坏等安全问题。例如,恶意用户可以编写脚本来删除重要数据,或者获取敏感信息。

资源管理

不同的脚本可能对 Redis 服务器的资源(如内存、CPU)有不同的需求。通过权限控制,可以限制某些用户或应用执行资源消耗较大的脚本,确保整个系统的稳定运行。

业务隔离

在企业级应用中,不同的业务模块可能使用同一个 Redis 实例。为了避免业务之间的相互干扰,需要对脚本执行进行权限划分,确保每个业务只能执行与自身相关的脚本。

基于用户角色的权限控制

一种常见的权限控制方式是基于用户角色。

角色定义

首先,定义不同的角色,例如:

  • 管理员角色:具有执行所有 Redis 脚本的权限。
  • 普通用户角色:只能执行特定的只读脚本,如获取数据的脚本。
  • 数据写入角色:可以执行写入数据相关的脚本,但不能执行删除数据等危险操作。

权限分配

对于管理员角色,可以将所有脚本的执行权限赋予该角色。假设我们有一个名为 admin 的角色,在权限管理系统(可以是自定义的系统或者与 Redis 集成的认证系统)中,将所有脚本的执行权限关联到 admin 角色。

对于普通用户角色,只允许执行特定的只读脚本。例如,有一个获取用户信息的只读脚本:

local user_key = KEYS[1]
return redis.call('HGETALL', user_key)

在权限管理系统中,将该脚本的执行权限分配给普通用户角色。

实现示例

假设我们使用一个简单的 Python 脚本来模拟权限验证和 Redis 脚本执行。首先安装 redis - py 库:

pip install redis

以下是示例代码:

import redis

# 连接 Redis
r = redis.Redis(host='localhost', port=6379, db = 0)

# 模拟权限验证函数
def has_permission(role, script_sha):
    # 这里只是简单模拟,实际应从权限数据库查询
    if role == 'admin':
        return True
    read_only_script_shas = ['abcdef1234567890'] # 只读脚本的 SHA1 摘要
    if role == '普通用户' and script_sha in read_only_script_shas:
        return True
    return False

# 加载脚本
script = "local user_key = KEYS[1] return redis.call('HGETALL', user_key)"
script_sha = r.script_load(script)

# 模拟用户角色
user_role = '普通用户'

if has_permission(user_role, script_sha):
    result = r.evalsha(script_sha, 1, 'user:1')
    print(result)
else:
    print('没有执行权限')

在上述代码中,has_permission 函数模拟了权限验证过程,根据用户角色和脚本的 SHA1 摘要判断是否有权限执行。

基于脚本内容的权限控制

除了基于用户角色,还可以根据脚本内容进行权限控制。

脚本内容分析

可以对 Lua 脚本进行词法和语法分析,提取出脚本中使用的 Redis 命令。例如,使用 Lua 的解析库(如 lua - parser)来解析脚本。假设我们有一个简单的 Lua 脚本:

local key = KEYS[1]
redis.call('SET', key, ARGV[1])
return redis.call('GET', key)

通过解析,可以提取出使用的 Redis 命令 SETGET

权限规则定义

根据提取出的命令,定义权限规则。例如:

  • 允许执行 GETHGETHGETALL 等只读命令的脚本。
  • 禁止执行 DELFLUSHALL 等危险命令的脚本。

实现示例

以下是使用 Python 和 lua - parser 库进行脚本内容分析和权限控制的示例。首先安装 lua - parser 库:

pip install lua - parser

示例代码如下:

import redis
import lua_parser

# 连接 Redis
r = redis.Redis(host='localhost', port=6379, db = 0)

# 定义允许的命令
allowed_commands = ['GET', 'HGET', 'HGETALL']

# 分析脚本内容
def analyze_script(script):
    ast = lua_parser.parse(script)
    commands = []
    for node in ast.walk():
        if isinstance(node, lua_parser.nodes.FunctionCall) and isinstance(node.func, lua_parser.nodes.Name) and node.func.id =='redis.call':
            command = node.args[0].s
            commands.append(command)
    return commands

# 加载脚本
script = "local key = KEYS[1] redis.call('SET', key, ARGV[1]) return redis.call('GET', key)"
script_commands = analyze_script(script)

is_allowed = all(command in allowed_commands for command in script_commands)

if is_allowed:
    script_sha = r.script_load(script)
    result = r.evalsha(script_sha, 1,'mykey', 'value')
    print(result)
else:
    print('脚本包含不允许的命令')

在上述代码中,analyze_script 函数解析 Lua 脚本,提取出使用的 Redis 命令,并与允许的命令列表进行比较,从而判断是否允许执行该脚本。

结合 ACL 进行权限控制

Redis 从 6.0 版本开始引入了 ACL(Access Control List)功能,可以与脚本管理的权限控制相结合。

ACL 基本概念

ACL 允许用户定义多个用户,每个用户有不同的权限集。可以通过 ACL SETUSER 命令来定义用户及其权限。例如:

ACL SETUSER user1 on >password1 +@all

上述命令定义了一个名为 user1 的用户,密码为 password1,具有所有权限(+@all)。

脚本相关权限设置

在 ACL 中,可以针对脚本执行设置特定权限。例如,定义一个只能执行特定脚本的用户:

ACL SETUSER script_user on >password2 +EVALSHA <script_sha1> <script_sha2>

这里 script_user 用户只能执行 SHA1 摘要为 script_sha1script_sha2 的脚本。

实现示例

以下是结合 ACL 和 Redis 客户端进行脚本执行权限控制的示例。假设使用 Python 的 redis - py 库:

import redis

# 连接 Redis
r = redis.Redis(host='localhost', port=6379, db = 0, username='script_user', password='password2')

# 加载脚本
script = "local key = KEYS[1] return redis.call('GET', key)"
script_sha = r.script_load(script)

try:
    result = r.evalsha(script_sha, 1,'mykey')
    print(result)
except redis.AuthenticationError:
    print('认证失败')
except redis.ResponseError as e:
    if 'EVALSHA' in str(e):
        print('没有执行该脚本的权限')
    else:
        raise e

在上述代码中,通过 redis - py 库使用特定用户连接 Redis,并尝试执行脚本。如果用户没有权限执行脚本,会捕获到 ResponseError 并提示没有执行权限。

权限控制中的缓存与更新

在权限控制实现过程中,缓存和权限更新是两个重要的方面。

权限缓存

为了提高权限验证的效率,可以对权限信息进行缓存。例如,将用户角色对应的权限列表缓存到内存中。假设使用 Python 的 functools.lru_cache 来缓存权限验证结果:

import redis
from functools import lru_cache

# 连接 Redis
r = redis.Redis(host='localhost', port=6379, db = 0)

# 模拟权限验证函数
@lru_cache(maxsize = 128)
def has_permission(role, script_sha):
    # 这里只是简单模拟,实际应从权限数据库查询
    if role == 'admin':
        return True
    read_only_script_shas = ['abcdef1234567890'] # 只读脚本的 SHA1 摘要
    if role == '普通用户' and script_sha in read_only_script_shas:
        return True
    return False

# 加载脚本
script = "local user_key = KEYS[1] return redis.call('HGETALL', user_key)"
script_sha = r.script_load(script)

# 模拟用户角色
user_role = '普通用户'

if has_permission(user_role, script_sha):
    result = r.evalsha(script_sha, 1, 'user:1')
    print(result)
else:
    print('没有执行权限')

在上述代码中,has_permission 函数使用 lru_cache 进行缓存,对于相同的 rolescript_sha 组合,会直接从缓存中返回结果,提高验证效率。

权限更新

当权限发生变化时,需要及时更新相关的缓存和权限配置。例如,当一个用户的角色发生变化,或者一个新的脚本被添加到允许执行的列表中时。假设使用 Redis 的发布 - 订阅机制来通知权限更新。

在权限更新的服务端(假设是 Python 代码):

import redis

r = redis.Redis(host='localhost', port=6379, db = 0)

# 发布权限更新消息
def publish_permission_update():
    r.publish('permission_updates', '权限已更新')

在权限验证的客户端(假设也是 Python 代码):

import redis
import time

r = redis.Redis(host='localhost', port=6379, db = 0)

# 订阅权限更新消息
pubsub = r.pubsub()
pubsub.subscribe('permission_updates')

# 处理权限更新消息
def handle_permission_update():
    for message in pubsub.listen():
        if message['type'] =='message':
            # 清除权限缓存
            has_permission.cache_clear()
            print('权限缓存已清除,重新加载权限')

# 启动订阅线程
import threading
thread = threading.Thread(target = handle_permission_update)
thread.start()

# 模拟权限验证函数
@lru_cache(maxsize = 128)
def has_permission(role, script_sha):
    # 这里只是简单模拟,实际应从权限数据库查询
    if role == 'admin':
        return True
    read_only_script_shas = ['abcdef1234567890'] # 只读脚本的 SHA1 摘要
    if role == '普通用户' and script_sha in read_only_script_shas:
        return True
    return False

在上述代码中,当权限更新消息发布后,客户端会收到消息并清除权限缓存,确保后续的权限验证使用最新的权限配置。

复杂场景下的权限控制策略

在一些复杂的企业级应用场景中,权限控制需要更加精细和灵活。

动态权限分配

在某些情况下,权限可能需要根据运行时的条件进行动态分配。例如,一个应用可能根据用户的登录时间、来源 IP 等因素来决定是否允许执行某个脚本。假设使用 Python 和 Flask 框架来实现一个简单的动态权限分配示例:

from flask import Flask, request
import redis

app = Flask(__name__)
r = redis.Redis(host='localhost', port=6379, db = 0)

# 模拟权限验证函数
def has_permission(role, script_sha, ip):
    # 这里只是简单模拟,实际应从权限数据库查询
    if role == 'admin':
        return True
    if role == '普通用户':
        trusted_ips = ['192.168.1.100']
        if ip in trusted_ips:
            read_only_script_shas = ['abcdef1234567890']
            if script_sha in read_only_script_shas:
                return True
    return False

# 加载脚本
script = "local user_key = KEYS[1] return redis.call('HGETALL', user_key)"
script_sha = r.script_load(script)

@app.route('/execute_script', methods = ['POST'])
def execute_script():
    role = request.json.get('role')
    ip = request.remote_addr
    if has_permission(role, script_sha, ip):
        result = r.evalsha(script_sha, 1, 'user:1')
        return str(result)
    else:
        return '没有执行权限'

if __name__ == '__main__':
    app.run(debug = True)

在上述代码中,has_permission 函数根据用户角色和请求的 IP 地址来判断是否有权限执行脚本。

多维度权限组合

在一些大型系统中,可能需要多个维度的权限组合来确定脚本执行权限。例如,结合用户角色、数据类型、操作类型等多个维度。假设我们有不同类型的数据(用户数据、订单数据等),不同的操作(读、写、删除等):

import redis

# 连接 Redis
r = redis.Redis(host='localhost', port=6379, db = 0)

# 模拟权限验证函数
def has_permission(role, data_type, operation_type, script_sha):
    # 这里只是简单模拟,实际应从权限数据库查询
    if role == 'admin':
        return True
    if role == '普通用户':
        if data_type == '用户数据' and operation_type == '读':
            read_only_script_shas = ['abcdef1234567890']
            if script_sha in read_only_script_shas:
                return True
    return False

# 加载脚本
script = "local user_key = KEYS[1] return redis.call('HGETALL', user_key)"
script_sha = r.script_load(script)

# 模拟参数
user_role = '普通用户'
data_type = '用户数据'
operation_type = '读'

if has_permission(user_role, data_type, operation_type, script_sha):
    result = r.evalsha(script_sha, 1, 'user:1')
    print(result)
else:
    print('没有执行权限')

在上述代码中,has_permission 函数根据用户角色、数据类型和操作类型来综合判断是否有权限执行脚本。

性能优化与注意事项

在实现 Redis 脚本管理命令的权限控制时,性能优化和一些注意事项是非常关键的。

性能优化

  1. 减少网络开销:尽量使用 EVALSHA 命令,通过预先加载脚本,减少每次执行脚本时的网络传输。例如,在一个高并发的应用中,如果频繁使用 EVAL 命令,网络带宽会成为瓶颈。而使用 EVALSHA 可以显著减少网络流量。
  2. 优化脚本本身:编写高效的 Lua 脚本,避免在脚本中进行不必要的循环和复杂计算。例如,尽量使用 Redis 提供的批量操作命令,如 MGETMSET 等,而不是在 Lua 脚本中逐个执行 GETSET 命令。
  3. 合理使用缓存:如前文所述,对权限验证结果进行缓存,可以大大提高验证效率。但是要注意缓存的更新策略,确保权限变化时缓存能够及时更新。

注意事项

  1. 脚本安全性:在解析和执行脚本时,要防止脚本注入攻击。例如,在使用外部输入构建 Lua 脚本时,要对输入进行严格的验证和过滤,避免恶意用户通过构造特殊的输入来执行恶意脚本。
  2. 兼容性:不同版本的 Redis 对脚本管理和 ACL 等功能的支持可能存在差异。在部署系统时,要确保所使用的 Redis 版本能够满足权限控制的需求,并且注意不同版本之间的兼容性问题。
  3. 监控与审计:建立对脚本执行的监控和审计机制,记录脚本的执行情况,包括执行时间、执行结果、执行用户等信息。这有助于在出现问题时进行排查,并且可以发现潜在的安全风险。

通过以上详细的介绍和示例,希望能够帮助读者全面理解和实现 Redis 脚本管理命令的权限控制,确保 Redis 应用在安全、稳定和高效的环境中运行。在实际应用中,需要根据具体的业务需求和系统架构,选择合适的权限控制策略,并不断优化和完善权限控制机制。