🪟 第16章 SQL Server数据库安全
📄 一句话概括

三条语句:GRANT 给、REVOKE 收回(回到「没授过」)、DENY 明确禁止(压过一切授权)。 REVOKE 不等于「不许」:它只是把票收回来,别人还能给他别的票;DENY 才是「禁入」。 实测:在角色里拿到 SELECT,再单独 DENY SELECT——结果还是拒绝。DENY 优先。

📝 本章和前面几章的关系

16.1 讲账号、16.2 讲角色,这一节讲最后一步:到底怎么发权限、怎么收。 教材 16.3 分四小节:权限分类、用户权限管理、角色权限管理、GRANT 与 REVOKE。 「权限分类」那一小节列的是对象权限、语句权限、隐含权限——知道有这三类就行,重点是后三个。 DENY 和 REVOKE 的区别是这一节的核心,也是最容易记混的地方。 演示里用复刻模型实测了「DENY 压过角色授权」,这个是真实语义。

一、权限分三类,别混

类别 例子 怎么给
对象权限 对表/视图/过程的 SELECT、INSERT、EXECUTE GRANT ... ON 对象
语句权限 CREATE TABLE、BACKUP DATABASE GRANT CREATE TABLE TO 用户
隐含权限 db_owner 天然能对库内对象做任何事 不能单独给,跟着角色来

隐含权限是「角色的附带效果」,不能单独授也不能单独收。 所以「为什么他能删表」有时候答不上来——因为他有 db_owner,而那是隐含的。 排查权限时最容易漏的就是这一类。 查 sys.database_permissions 只看到显式授权, 隐含权限要按角色去推——这也是为什么 16.2 建议「角色别套太深」。

二、三条语句:给、收回、禁止

语句 作用 容易记错的地方
GRANT 给权限 ——
REVOKE 收回授权(回到「没授过」的状态) 它不等于「禁止」
DENY 明确禁止(优先于任何 GRANT) 这才是「不许」

REVOKE 和 DENY 的区别是这一节最重要的一句话: REVOKE 是「撤销我发出去的这张票」,DENY 是「在门口挂一块『此人禁入』的牌子」。 前者别人还能给他别的票,后者谁的票都不好使。 换个说法更好记: REVOKE 之后是「无授权」状态——结果可能还是能进(因为他还在某个角色里); DENY 之后是「明确拒绝」——谁给票都不行。

三、授权与收回:先给再撤

操作 结果
GRANT SELECT ON tb_order TO u_zhang 允许
再来一句 REVOKE ... FROM u_zhang 拒绝(无授权)

收回之后是「无授权」,不是「禁止」。 这个状态跟「从来没授过权」一模一样—— 所以如果他还在某个角色里,角色给的权限照样有效。 这也解释了一种常见困惑: 「我明明 REVOKE 了,他怎么还能查? 」因为他从角色那儿拿到的权限没被撤销——REVOKE 只撤你自己发出去的那张票。 要真正挡住,用 DENY。 但用之前最好确认一下自己在做什么——下一段就是它的威力。

四、DENY 优先:既是最好用的工具,也是事故源头

u_zhang 在一个角色里拿到了 SELECT,再单独给他 DENY SELECT:

状态 结果
只有角色授权 允许
再加一个 DENY SELECT 拒绝(DENY 优先)

DENY 压过一切 GRANT,不管那个 GRANT 是谁给的。 所以「怎么把人挡在某个表外面」的正确答案是 DENY,不是 REVOKE。 反过来说,这也是事故源头: 有人给某个角色 DENY SELECT, 结果角色里所有人——包括 db_owner——都被挡住了(只有 sysadmin 例外)。 排查「明明授权了怎么还读不了」时,第一件事是查 sys.database_permissions 里的 DENY 行。 这个查询比猜快一百倍。

五、权限的粒度:库、表、列、过程

粒度 写法 什么时候用
整库 GRANT SELECT TO u_li(当前库所有对象) 很少用,太宽
表 GRANT SELECT ON tb_order TO u_li 最常用
列 GRANT SELECT (order_no, total) ON tb_order TO u_li 要藏敏感列时
存储过程 GRANT EXECUTE ON proc_x TO u_li 只让调过程、不给看表

列级权限有个「反效果」: 只给 SELECT (order_no), 那么 SELECT * 会直接报错——因为 * 包含了没授权的列。 所以列级权限往往配合视图用(10.1 讲过):建一个只含允许列的视图,给人视图的权限, 比给列权限干净得多——用户不会踩 SELECT * 的坑,你也不用维护一堆列级授权。 存储过程权限是最实用的那一招: 只给 EXECUTE、不给表的 SELECT, 用户能调过程拿到结果,但不能自己写 SELECT 到处翻。 这是「最小权限」最好落地的形态,而且「接口和权限一起被封装」了。

六、WITH GRANT OPTION:能转授权,慎给

GRANT SELECT ON tb_order TO u_li WITH GRANT OPTION;   -- u_li 还能转授给别人
REVOKE SELECT ON tb_order FROM u_li CASCADE;           -- 转授出去的一起收回来
GO

WITH GRANT OPTION 的意思是「我给的票,你可以再发出去」。 方便,但也很危险: 你收回的时候,他发出去的那些默认不收回(会报错), 得加 CASCADE 才能连锁收回。 这相当于「权限的传销」: 一个人能转授,你就再也说不清权限扩散到哪了。 所以这条几乎只在「部门管理员管本部门」这种场景下用,而且要配审计。

七、动手 Demo:GRANT、REVOKE、DENY:三个动词一台戏

这一段用复刻模型走三步:先 GRANT 让 u_zhang 能读、再 REVOKE 看它回到什么状态, 最后通过角色授权 + 单独 DENY,实测「DENY 优先」这条规矩。 中间那张「只有角色授权 vs 再加 DENY」的对照,是整章最该记住的一组结果。

# ① 直接授权,再收回
s.grant_to(u"u_zhang", u"tb_order", u"SELECT")
print(s.check(u"u_zhang", u"tb_order", u"SELECT"))    # 允许
s.revoke(u"u_zhang", u"tb_order", u"SELECT")
print(s.check(u"u_zhang", u"tb_order", u"SELECT"))    # 拒绝(无授权)

# ② 通过角色授权,再单独 DENY
s.roles[u"r_reader"] = set()
s.add_member(u"r_reader", u"u_zhang")
s.grant_to(u"r_reader", u"tb_order", u"SELECT")
print(s.check(u"u_zhang", u"tb_order", u"SELECT"))    # 允许(角色给的)
s.deny_to(u"u_zhang", u"tb_order", u"SELECT")
print(s.check(u"u_zhang", u"tb_order", u"SELECT"))    # 拒绝(DENY 优先)

实跑输出:

--------------------------------------------------------------------
权限管理:GRANT、REVOKE、DENY
--------------------------------------------------------------------


  ① 三条语句,别记混
  --------------------------------------------------------------------
| 语句 | 作用 | 容易记错的地方 |
|---|---|---|
| GRANT | 给权限 | —— |
| REVOKE | 收回授权(回到「没授过」的状态) | 它不等于「禁止」 |
| DENY | 明确禁止(优先于任何 GRANT) | 这才是「不许」 |

  REVOKE 和 DENY 的区别是这一节最重要的一句话:
  REVOKE 是「撤销我发出去的这张票」,DENY 是「在门口挂一块『此人禁入』的牌子」。
  前者别人还能给他别的票,后者谁的票都不好使。

  ② 授权与收回:先给再撤
  --------------------------------------------------------------------

| 操作 | 结果 |
|---|---|
| GRANT SELECT ON tb_order TO u_zhang | 允许 |

  执行 `REVOKE SELECT ON tb_order FROM u_zhang` 之后:
    拒绝(无授权)
  回到「无授权」。 注意它并不是「禁止」——只是没票了。
  如果这个人还在某个角色里,角色给的票照样有效。

  ③ DENY 优先:这是最容易踩的坑
  --------------------------------------------------------------------
  u_zhang 在一个角色里拿到了 SELECT,再单独给他 DENY SELECT:

  只有角色授权时:允许
  再加一个 DENY 之后:拒绝(DENY 优先)
  DENY 压过一切 GRANT,不管那个 GRANT 是谁给的。
  所以「怎么把人挡在某个表外面」的正确答案是 DENY,不是 REVOKE。
  反过来说,这也是事故源头: 有人给某个角色 DENY SELECT,结果角色里所有人、
  包括 db_owner 都被挡住了(sysadmin 除外)——
  排查「明明授权了怎么还读不了」时,先查 sys.database_permissions 里的 DENY 行。

  ④ 权限的粒度:库、表、列
  --------------------------------------------------------------------
| 粒度 | 写法 | 什么时候用 |
|---|---|---|
| 整库 | GRANT SELECT TO u_li(当前库所有对象) | 很少用,太宽 |
| 表 | GRANT SELECT ON tb_order TO u_li | 最常用 |
| 列 | GRANT SELECT (order_no, total) ON tb_order TO u_li | 要藏敏感列时 |
| 存储过程 | GRANT EXECUTE ON proc_x TO u_li | 只让调过程、不给看表 |

  列级权限有个「反效果」要注意: 只给 SELECT (order_no),
  那么 SELECT * 会直接报错——因为 * 包含了没授权的列。
  所以列级权限往往配合视图用(10.1 讲过):建一个只含允许列的视图,给人视图的权限,
  比给列权限干净得多。

  ⑤ 存储过程权限:最实用的那一招
  --------------------------------------------------------------------
  只给 EXECUTE,不给表的 SELECT——用户能调过程、拿到结果,但不能自己写 SELECT 到处翻。
  这是「最小权限」最好落地的形态。
  它还有个额外好处: 表结构变了,只要过程的输出不变,调用方不受影响。权限和接口一起被封装了。

  ⑥ 看看谁有什么权限
  --------------------------------------------------------------------

  [教材原码 · 本沙箱不执行]
  | SELECT pr.name AS 主体, o.name AS 对象, p.permission_name, p.state_desc
  | FROM sys.database_permissions p
  | JOIN sys.database_principals pr ON pr.principal_id = p.grantee_principal_id
  | LEFT JOIN sys.objects o ON o.object_id = p.major_id
  | ORDER BY pr.name, o.name;
  | GO

  state_desc 那一列会告诉你它是 GRANT 还是 DENY。排查权限问题,第一件事就是跑这条查询——别靠猜。
  顺带一句:「这个人到底能干什么」的完整答案要算上角色继承,
  单纯查 sys.database_permissions 只看到直接授权的部分,
  所以更省事的办法是 EXECUTE AS USER = 'u_zhang' 然后自己试一下。

四行输出正好是四个状态:允许 → 无授权 → 允许 → 拒绝。 第三步那个「允许」来自角色,不是来自直接授权—— 这说明 REVOKE 之后再授权,路径可能完全不同。 第四步是这一节的结论:DENY 一挂上,角色给的票立刻作废。 check() 里先查 deny、再查 grant,就是这个规矩的代码形态。 真实 SQL Server 里 DENY 还有个细节: 它只对当前库有效, 而且 sysadmin 不受影响——系统管理员永远能绕过任何 DENY。 所以别指望用 DENY 去限制一个 sysadmin。 还有一个隐蔽的坑: DENY 也会影响「他以另一个身份执行」的场景。 如果存储过程里用 EXECUTE AS OWNER 提升了权限,而用户本身被 DENY 了, 那个 DENY 依然可能生效——排查这类问题只能靠 EXECUTE AS USER 实测。

30 秒实验

  1. 把 DENY 换成 REVOKE(撤掉角色的授权),看结果。 会变成「拒绝(无授权)」——同样是拒绝,原因不同:一个是「禁入牌子」,一个是「没票」。
  2. 给 u_li 授权时加一句 WITH GRANT OPTION 的模拟(在模型里记一个标记),让他转授给 u_wang。 u_wang 也能读了——这就是「权限扩散」的开端。
  3. 想一想:一个报表账号要读 20 张表、但不许读其中 2 张,你会怎么写? 先把 20 张表的读权限给角色,再对那 2 张 DENY—— 这比「逐个 GRANT 18 张」好维护,但记得在文档里写清那两个 DENY。

常见误区

  1. 「REVOKE 就是禁止」。 不是。 REVOKE 只是收回你发出去的授权,回到「没授过」; 他若还在角色里,照样有权限。真正禁止要用 DENY。
  2. 「DENY 只影响直接授权的人」。 它压过一切 GRANT,包括角色给的——所以给角色挂 DENY 会连累所有人。 只有 sysadmin 不受影响。
  3. 「列级权限比视图方便」。列级权限会让 SELECT * 直接报错,用的人一多就到处踩坑; 视图能控制「看起来长什么样」,比列级授权干净得多。
自测三问(点开对答案) 1. `REVOKE` 和 `DENY` 有什么本质区别? —— **`REVOKE` 是「把票收回来」**,回到「没授过」的状态,别人还能给他别的票; **`DENY` 是「挂一块禁入牌」**,压过一切授权。 2. 角色已经给了 `SELECT`,再单独 `DENY SELECT` 会怎样? —— **结果是拒绝。** `DENY` 优先于任何 `GRANT`,不管那个授权来自角色还是直接给的。 **只有 `sysadmin` 不受影响。** 3. 为什么「只给存储过程 `EXECUTE`」是最实用的授权方式? —— **用户能拿到结果,但不能自己写 `SELECT` 到处翻表**—— **权限和接口一起被封装,表结构改了调用方也不受影响。**

下一篇 16.4 讲加密:为什么密码只能哈希不能加密、盐到底防的是什么, 以及「主密钥 → 证书 → 对称密钥」这三层套娃各管什么。

相关笔记