Azure SQL中如何为存储过程授予调用用户不具备的权限
Azure SQL存储过程小时级执行频次限制方案
已完成的基础配置
- 已在目标业务数据库(非master系统库)创建专属受限用户,创建语句如下:
CREATE USER sp_only_user WITH PASSWORD = 'blabla12345!@#$'
- 已仅为该用户授予目标存储过程
sp_Get_Clients的执行权限:
GRANT EXECUTE ON OBJECT::sp_Get_Clients to sp_only_user
待解决的核心问题
需要在存储过程内部校验自身过去1小时的执行记录,可选实现方式共两种,要求不向普通调用用户授予系统性能视图、统计函数的直接访问权限,仅让存储过程本身具备对应查询能力:
- 直接在存储过程内查询系统性能视图统计最近执行时间,查询语句如下,禁止普通用户直接执行该语句:
SELECT @LastExecutionTime= PS.last_execution_time FROM sys.dm_exec_procedure_stats PS INNER JOIN sys.objects o ON O.[object_id] = PS.[object_id] WHERE name = 'sp_Get_Clients'
- 将统计逻辑封装为自定义函数
dbo.fn_CountSpExecutionsPerHour,返回指定存储过程的小时级执行次数,存储过程内调用语句如下:
SELECT @ExecutionCntPerHour = dbo.fn_CountSpExecutionsPerHour('sp_Get_Clients')
需要确认两个核心点:
- 是否可以为存储过程授予调用者本身不具备的访问权限
- 是否存在更简便、可靠的存储过程小时级执行频次限制方案
解决方案
1. 跨权限访问实现:模块证书签名(无需给用户授予额外权限)
完全可以实现存储过程持有调用者不具备的权限,不要用EXECUTE AS OWNER这类上下文切换方案——该方案会让存储过程全程以高权限所有者身份运行,一旦存在SQL注入漏洞,会直接泄露高权限,安全风险极高。
官方推荐的标准实现是模块证书签名,仅给对应模块授予完成操作所需的最小权限,步骤如下:
- 在目标业务库创建自定义证书
CREATE CERTIFICATE SpExecAccessCert ENCRYPTION BY PASSWORD = 'CertPassword_2024!@#' WITH SUBJECT = 'Certificate for procedure execution stats access', EXPIRY_DATE = '2099-12-31';
- 给需要访问DMV的对象添加证书签名:如果选择直接在存储过程内写DMV查询,就给存储过程签名;如果选择封装统计函数的方案,就给统计函数签名(同Schema下的存储过程调用函数走所有权链,无需额外授权)
-- 给存储过程签名示例 ADD SIGNATURE TO OBJECT::sp_Get_Clients BY CERTIFICATE SpExecAccessCert WITH PASSWORD = 'CertPassword_2024!@#';
- 将证书映射为数据库用户,仅给该用户授予访问DMV必需的
VIEW SERVER STATE权限
CREATE USER SpExecAccessCertUser FROM CERTIFICATE SpExecAccessCert; GRANT VIEW SERVER STATE TO SpExecAccessCertUser;
配置完成后,普通用户执行存储过程时,签名模块会自动携带证书用户的权限查询DMV,用户本身没有任何直接访问系统视图、统计函数的权限,符合最小权限要求。
注意:后续如果修改了存储过程、统计函数的定义,需要重新执行签名步骤,否则签名会自动失效。
2. 生产环境推荐方案:自建执行日志表(不要依赖DMV做频次校验)
sys.dm_exec_procedure_stats的数据存储在执行计划缓存中,完全不适合做业务层的频次校验:SQL实例重启、Azure SQL故障转移、执行计划缓存淘汰、存储过程重编译都会清空该视图的统计数据,直接导致频次校验失效。
更可靠、实现更简单的方案是自行维护轻量执行日志表,全程走所有权链,不需要任何服务器级权限:
- 创建执行日志表
CREATE TABLE dbo.SpExecutionLog( SpName SYSNAME NOT NULL, ExecuteUser SYSNAME NOT NULL, ExecuteTime DATETIME2(0) NOT NULL DEFAULT(SYSUTCDATETIME()), INDEX IX_SpExecutionLog_Query (SpName, ExecuteUser, ExecuteTime) );
- 在存储过程开头加入频次校验逻辑,原有业务逻辑放在校验之后即可
CREATE OR ALTER PROCEDURE dbo.sp_Get_Clients AS BEGIN SET NOCOUNT ON; DECLARE @ExecCnt INT; -- 记录本次执行 INSERT INTO dbo.SpExecutionLog(SpName, ExecuteUser) VALUES('sp_Get_Clients', ORIGINAL_LOGIN()); -- 统计当前用户近1小时执行次数 SELECT @ExecCnt = COUNT(*) FROM dbo.SpExecutionLog WHERE SpName = 'sp_Get_Clients' AND ExecuteUser = ORIGINAL_LOGIN() AND ExecuteTime > DATEADD(HOUR, -1, SYSUTCDATETIME()); -- 超过阈值直接终止,阈值可根据需求调整 IF @ExecCnt > 1 BEGIN RAISERROR('该存储过程每小时仅允许执行1次',16,1); RETURN; END -- 原有业务查询逻辑写在此处 -- SELECT xxx FROM xxx END
- 仅需给受限用户授予日志表的插入权限即可,因为存储过程和日志表同属dbo架构,走所有权链,存储过程内查询日志表不需要给用户授予SELECT权限,用户完全无法查看日志内容,符合安全要求:
GRANT INSERT ON dbo.SpExecutionLog TO sp_only_user;
可定期添加作业清理日志表的历史数据,避免表体积过大影响性能。
内容的提问来源于stack exchange,提问作者user1372603
相关产品推荐
相关产品推荐

