SQL Server存储过程无内置只读开关,实现只读需三重保障:权限隔离(移除db_datawriter、显式DENY写权限)、EXECUTE AS切换至仅SELECT权限的专用用户、严格审查视图/同义词/跨库依赖及禁用TRUSTWORTHY。

SQL Server 存储过程本身没有「只读开关」,所谓只读,是让执行者在调用时无法对任何表执行 INSERT、UPDATE、DELETE——哪怕过程体里写了这些语句,也必须让它运行失败。核心靠三件事:权限隔离 + 执行上下文控制 + 依赖链审查。
只给 EXECUTE 权限远远不够
常见错误是只运行了这句:
GRANT EXECUTE ON OBJECT::usp_GetSalesData TO [app_user];
但 app_user 如果还属于 db_datawriter 角色,或者对某张表有单独的 UPDATE 权限,那过程里任何 UPDATE Orders SET ... 都会成功执行。
- 先移除角色:
ALTER ROLE db_datawriter DROP MEMBER [app_user] - 再显式拒绝写权限(比
REVOKE更强):DENY INSERT, UPDATE, DELETE ON SCHEMA::dbo TO [app_user] - 检查是否残留
db_owner或sysadmin角色——这类高权限会完全绕过对象级限制
用 EXECUTE AS 强制切换到只读上下文
即使用户权限已清理,仍可能因过程内部动态 SQL 或跨库引用意外触发写操作。最稳妥的方式是在存储过程开头主动降权:
CREATE PROCEDURE usp_GetSalesData WITH EXECUTE AS 'readonly_proxy' AS BEGIN SELECT * FROM Sales.SalesOrderHeader; -- 即使这里误写了 UPDATE,也会因 readonly_proxy 没权限而报错 END
-
readonly_proxy必须是真实存在的数据库用户,且只被授予SELECT权限(如加入db_datareader) - 不能是
dbo或拥有IMPERSONATE权限的账户,否则无效 - 如果过程需查
sys.tables等系统视图,额外加:GRANT VIEW DEFINITION TO readonly_proxy
小心视图、同义词和跨库依赖
权限检查发生在运行时,且逐层下推。如果 usp_GetSalesData 查询一个视图 v_SalesSummary,而该视图定义里 JOIN 了用户有写权限的 Logs.AuditTrail 表,SQL Server 仍会检查 AuditTrail 的 SELECT 权限——但若用户对该表有 UPDATE 权限,就说明权限模型已失控。
- 用
SELECT definition FROM sys.sql_modules WHERE object_id = OBJECT_ID('v_SalesSummary')查视图逻辑 - 用
SELECT referenced_database_name, referenced_schema_name, referenced_entity_name FROM sys.dm_exec_describe_first_result_set(N'EXEC usp_GetSalesData', NULL, 0)粗略看依赖(注意它不覆盖所有动态路径) - 真正可靠的是在测试环境以
readonly_proxy身份手动执行,观察是否报Msg 229, Level 14(权限拒绝)而非静默成功
最容易被跳过的点:TRUSTWORTHY 数据库属性或证书签名。如果数据库设为 TRUSTWORTHY ON,或过程用证书签名并授了 UNSAFE ASSEMBLY 权限,EXECUTE AS 可能被绕过。生产环境务必关掉:ALTER DATABASE [YourDB] SET TRUSTWORTHY OFF。

















