三条语句: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 秒实验
- 把
DENY换成REVOKE(撤掉角色的授权),看结果。 会变成「拒绝(无授权)」——同样是拒绝,原因不同:一个是「禁入牌子」,一个是「没票」。 - 给
u_li授权时加一句WITH GRANT OPTION的模拟(在模型里记一个标记),让他转授给u_wang。u_wang也能读了——这就是「权限扩散」的开端。 - 想一想:一个报表账号要读 20 张表、但不许读其中 2 张,你会怎么写?
先把 20 张表的读权限给角色,再对那 2 张
DENY—— 这比「逐个GRANT18 张」好维护,但记得在文档里写清那两个DENY。
常见误区
- 「
REVOKE就是禁止」。 不是。REVOKE只是收回你发出去的授权,回到「没授过」; 他若还在角色里,照样有权限。真正禁止要用DENY。 - 「
DENY只影响直接授权的人」。 它压过一切GRANT,包括角色给的——所以给角色挂DENY会连累所有人。 只有sysadmin不受影响。 - 「列级权限比视图方便」。列级权限会让
SELECT *直接报错,用的人一多就到处踩坑; 视图能控制「看起来长什么样」,比列级授权干净得多。
自测三问(点开对答案)
1. `REVOKE` 和 `DENY` 有什么本质区别? —— **`REVOKE` 是「把票收回来」**,回到「没授过」的状态,别人还能给他别的票; **`DENY` 是「挂一块禁入牌」**,压过一切授权。 2. 角色已经给了 `SELECT`,再单独 `DENY SELECT` 会怎样? —— **结果是拒绝。** `DENY` 优先于任何 `GRANT`,不管那个授权来自角色还是直接给的。 **只有 `sysadmin` 不受影响。** 3. 为什么「只给存储过程 `EXECUTE`」是最实用的授权方式? —— **用户能拿到结果,但不能自己写 `SELECT` 到处翻表**—— **权限和接口一起被封装,表结构改了调用方也不受影响。**下一篇 16.4 讲加密:为什么密码只能哈希不能加密、盐到底防的是什么, 以及「主密钥 → 证书 → 对称密钥」这三层套娃各管什么。
- ☐ (本篇完,下一篇 16.4 数据加密与解密)