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

角色是「权限的包」:把权限发给角色、把人塞进角色,改权限只改一处。 固定服务器角色改不了权限,只能往里塞人;securityadmin 其实接近 sysadmin。 自定义角色能当成员互相嵌套,但最多两层——套深了没人看得懂,排错是灾难。

📝 本章和前面几章的关系

16.1 讲「怎么建账号」,这一节讲「怎么成批管权限」。 账号一多,逐个授权就没人管得住了。 教材 16.2 分三小节:固定服务器角色、数据库角色、自定义角色。 前两个是「装好就有、改不了内容」的,第三个才是你能自己设计的。 SQLite 没有角色,所以继承关系是用 Python 模型真算的: 「成员 → 角色 → 权限」逐层展开,结果能一眼看出谁行谁不行。

一、为什么要角色:不打包就得一个个发

五个人的权限一样,逐个 GRANT 就是五遍;来第六个人,再发一遍。 角色就是「权限的包」:把权限发给角色,把人塞进角色。

好处 具体是什么
加人一次 只改成员关系,不动权限
改权限一处 改了角色,所有成员同时生效
看得懂 看角色名就知道这人能干什么,不用翻授权表

第三条最被低估。 一个库上百张表、几十个账号, 没有角色的话,「这个人能干什么」这个问题根本答不上来—— 有角色就能答:「他在 r_sales_read 里,能读订单和商品」。

二、固定服务器角色:装好就有,改不了权限

角色 能干什么
sysadmin 服务器上什么都能干(sa 就在这个角色里)
securityadmin 管登录名和密码(能给自己提权的角色,慎给)
dbcreator 建库、改库、删库
bulkadmin 执行 BULK INSERT 批量导入
processadmin 杀会话(KILL)

固定角色的权限是写死的,你只能「把人塞进去」,不能改它有什么权限。 这是好事:它不会被误改,也不会因为某个人的需要被放宽。 securityadmin 要特别当心: 它能管登录名, 等于能给自己开任何权限——所以它实际上接近 sysadmin,别当成「只管账号的小角色」。 这是一类典型的误判: 看名字觉得「管安全的人权限应该有限」, 实际上「能改密码」本身就等于「能装成任何人」。 给这个角色之前先想清楚:这个人可以随时变成 sa。

三、数据库角色:库内的一套

角色 能干什么
db_owner 这个库里的神:建表、删表、授权都行
db_datareader 能读所有表(SELECT)
db_datawriter 能写所有表(增删改)
db_denydatareader 拒绝读所有表(DENY 优先,会盖掉上面的授权)
db_ddladmin 能改表结构,但不能动数据

这几个角色是「按库」给的——同一个账号在 A 库是读者、在 B 库可以是写者。 这就是 16.1 说的「用户是数据库级」的另一面。 db_denydatareader 值得单说: 它是「拒绝读」,而DENY 优先于任何 GRANT。 所以把某人塞进这个角色,他连自己表都读不了—— 哪怕有人单独给过他 SELECT。 16.3 会实测这个优先级。 注意 db_owner 挡不住 sysadmin。 系统管理员在任何库里都是最高权限, 所以「这个库我只给 db_owner」这句话,在 sysadmin 面前不成立。

四、自定义角色:按业务打包

USE sales;
CREATE ROLE r_sales_read;
GRANT SELECT ON tb_order TO r_sales_read;
GRANT SELECT ON tb_goods TO r_sales_read;
EXEC sp_addrolemember 'r_sales_read', 'u_zhang';   -- 老写法
ALTER ROLE r_sales_read ADD MEMBER u_li;             -- 新写法(2012 之后)
GO

复刻模型跑出来的继承结果:

谁 tb_order 的 SELECT tb_goods 的 SELECT
u_zhang(在角色里) 允许 允许
u_li(在角色里) 允许 允许
u_wang(不在角色里) 拒绝(无授权) 拒绝(无授权)

两行「允许」、一行「无授权」——角色就是这么工作的。 注意 u_wang 是「无授权」不是「拒绝」,这个区别在 16.3 非常关键: 「没票」和「禁入」是两件事。 角色名怎么起?按「业务 + 权限」起,别按部门起。 r_sales_read 比 r_销售部 好——人是会调岗的,权限不该跟着部门走。

五、角色能套角色:别套太深

SQL Server 允许角色包含角色(ALTER ROLE r_outer ADD MEMBER r_inner), 权限会一路继承下去。 复刻模型里的 principals() 就是递归展开的。 但套深了没人看得懂。 一个账号能不能查某张表,得顺着三层角色往上翻——排错时这是灾难, 尤其是「明明授了权却读不到」这种问题。 经验值:角色最多两层(业务角色 → 基础角色),再多就该拆。 判断标准很简单: 你能不能一句话说清这个角色的权限范围?说不清就是套太深了。 还有个坑: 成员关系加进去立刻生效,不需要重新登录; 但权限相关的执行计划可能要等下次执行才重新编译—— 所以「刚授的权没生效」先别怀疑语句写错,等一两秒再试。

六、动手 Demo:从「成员」到「权限」:继承是怎么算的

这一段用复刻模型演示角色的继承: 建一个 r_sales_read 角色、给它两个表的读权限、塞两个人进去、再检查三个账号能不能查。 关键是最后一行 u_wang:他在角色外,结果是「无授权」而不是「拒绝」—— 这两个词在下一篇会分开讲。

class Sec(object):
    # ... 16.1 的登录/用户部分省略 ...

    def principals(self, name):
        """一个主体直接或间接所属的全部主体(自己 + 递归角色)。"""
        out, stack = {name}, [name]
        while stack:
            cur = stack.pop()
            for r, mem in self.roles.items():
                if cur in mem and r not in out:
                    out.add(r)
                    stack.append(r)
        return out

    def check(self, uname, obj, action):
        who = self.principals(uname)
        if any((p, obj, action) in self.deny for p in who):
            return u"拒绝(DENY 优先)"
        if any((p, obj, action) in self.grant for p in who):
            return u"允许"
        return u"拒绝(无授权)"


s.roles[u"r_sales_read"] = set()
s.add_member(u"r_sales_read", u"u_zhang")
s.add_member(u"r_sales_read", u"u_li")
s.grant_to(u"r_sales_read", u"tb_order", u"SELECT")
s.grant_to(u"r_sales_read", u"tb_goods", u"SELECT")
for u in (u"u_zhang", u"u_li", u"u_wang"):
    print(u, s.check(u, u"tb_order", u"SELECT"))

实跑输出:

--------------------------------------------------------------------
角色管理:把权限打包发给一群人
--------------------------------------------------------------------


  ① 为什么要角色:不打包就得一个个发
  --------------------------------------------------------------------
  五个人的权限一样,逐个 GRANT 就是五遍;来第六个人,再发一遍。
  角色就是「权限的包」:把权限发给角色,把人塞进角色。
  好处有三个: 加人只加一次成员关系;改权限只改一处;看角色就知道这人能干什么。

  ② 固定服务器角色:装好就有,改不了权限
  --------------------------------------------------------------------
| 角色 | 能干什么 |
|---|---|
| sysadmin | 服务器上什么都能干(sa 就在这个角色里) |
| securityadmin | 管登录名和密码(能给自己提权的角色,慎给) |
| dbcreator | 建库、改库、删库 |
| bulkadmin | 执行 BULK INSERT 批量导入 |
| processadmin | 杀会话(KILL) |

  固定角色的权限是写死的,你只能「把人塞进去」,不能改它有什么权限。这是好事:它不会被误改。
  securityadmin 要特别当心: 它能管登录名,等于能给自己开任何权限——所以它实际上接近 sysadmin,
  别当成「只管账号的小角色」。

  ③ 数据库角色:库内的一套
  --------------------------------------------------------------------
| 角色 | 能干什么 |
|---|---|
| db_owner | 这个库里的神:建表、删表、授权都行 |
| db_datareader | 能读所有表(SELECT) |
| db_datawriter | 能写所有表(增删改) |
| db_denydatareader | 拒绝读所有表(DENY 优先,会盖掉上面的授权) |
| db_ddladmin | 能改表结构,但不能动数据 |

  这几个角色是「按库」给的,同一个账号在 A 库是读者、在 B 库可以是写者。
  这也是为什么「用户」和「登录名」要分开:登录名只有一个,用户可以在每个库里不一样。

  ④ 自定义角色:按业务打包
  --------------------------------------------------------------------

  [教材原码 · 本沙箱不执行]
  | USE sales;
  | CREATE ROLE r_sales_read;
  | GRANT SELECT ON tb_order TO r_sales_read;
  | GRANT SELECT ON tb_goods TO r_sales_read;
  | EXEC sp_addrolemember 'r_sales_read', 'u_zhang';   -- 老写法
  | ALTER ROLE r_sales_read ADD MEMBER u_li;             -- 新写法(2012 之后)
  | GO

  复刻模型里跑一遍「成员 → 角色 → 权限」的继承:

| 谁 | 对 tb_order 能 SELECT 吗 | 对 tb_goods 能 SELECT 吗 |
|---|---|---|
| u_zhang(在角色里) | 允许 | 允许 |
| u_li(在角色里) | 允许 | 允许 |
| u_wang(不在角色里) | 拒绝(无授权) | 拒绝(无授权) |

  两行「允许」、一行「无授权」——角色就是这么工作的。
  注意 u_wang 是「无授权」不是「拒绝」,这个区别在下一段很关键。

  ⑤ 角色能套角色:别套太深
  --------------------------------------------------------------------
  SQL Server 允许角色包含角色(ALTER ROLE r_outer ADD MEMBER r_inner),权限会一路继承下去。
  复刻模型里也是这么算的——principals() 会递归展开。
  但套深了没人看得懂。 一个账号能不能查某张表,得顺着三层角色往上翻——排错时这是灾难。
  经验值:角色最多两层(业务角色 → 基础角色),再多就该拆了。
  还有一个坑: 角色成员关系是「加进去就生效」的,不需要重新登录;
  但权限缓存可能要等一会儿(SQL Server 会在下次执行时重新编译)。
  所以「刚授的权没生效」先别怀疑语句写错了。

principals() 那十几行就是「继承」的全部实现: 从一个主体出发,往上找它所属的角色、再找角色所属的角色,直到找不出新的。 SQL Server 内部也是这个思路(它把结果算好缓存起来)。 check() 里的顺序很关键:先查 deny、再查 grant。 这个顺序就是「DENY 优先」的落地方式——只要命中一条拒绝,直接返回,后面的授权看都不看。 「拒绝(无授权)」和「拒绝(DENY 优先)」是两种不同的拒绝。 排错时这个区别决定你往哪查: 前者去找「谁该授权没授」,后者去找「谁挂了个 DENY」。 复刻模型删掉了所有「真实数据库的细节」(权限缓存、schema、所有权链), 留下的是「谁能干什么」这一层语义——理解了这一层,再去记 SQL Server 的具体语法就快得多。

30 秒实验

  1. 把 u_li 从 r_sales_read 里移除,再检查一次。 他变成「拒绝(无授权)」——这就是「把人从角色里拿出来」的效果,比逐个 REVOKE 干净。
  2. 建一个 r_all 角色,把它加进 r_sales_read 的成员,看 u_zhang 的 principals() 返回什么。 会多一个 r_all——这就是「角色套角色」的展开。
  3. 想一想:如果一个人既要「读订单」又要「不能看金额列」,你会怎么设计? 别用列级权限硬拦——建一个不含金额列的视图,给他视图的读权限, 干净、好维护、也不会因为 SELECT * 报错。

常见误区

  1. 「securityadmin 是个小角色」。 它能改密码、能建登录名,等于能装成任何人——实际权限接近 sysadmin。
  2. 「把角色套起来更灵活」。 灵活是真的,看不懂也是真的。 三层以上,「这人为什么能读这张表」就没人答得上来。 经验值两层。
  3. 「给了 db_owner 就什么都能干」。 在 sysadmin 面前不成立。 系统管理员在任何库里都最高, 所以别指望用库角色去限制一个服务器级管理员。
自测三问(点开对答案) 1. 固定服务器角色和数据库角色差在哪? —— **固定服务器角色管整台服务器**(建库、管登录名、杀会话),**权限写死改不了**; **数据库角色只管一个库里的对象**(读、写、改结构)。 2. 为什么说 `securityadmin` 接近 `sysadmin`? —— **因为它能改密码、能建登录名**——**能改别人的密码就等于能装成别人登录**, **等于能给自己开任何权限。** 3. 角色最多套几层比较合适?为什么? —— **两层左右。** 套深了**「这人为什么有权限」要顺着好几层翻**, **排错成本会超过它带来的灵活性。**

下一篇 16.3 讲授权语句: GRANT、REVOKE、DENY 三者到底差在哪,以及为什么「DENY 优先」既是最好用的工具, 也是最常见的事故源头。

相关笔记