两层钥匙:登录名(LOGIN,服务器级)管「能不能连上来」,用户(USER,
库级)管「能进哪个库」。
只有登录名没有用户,连得上服务器、进不了库;只有用户没有登录名,那就是「孤儿用户」。
删登录名之前必须先删各库里的用户——不然 SQL Server 会报「用户正被使用」。
前五章(14~15)讲的是「数据别变脏、别丢」,这一章换个角度:谁能看、谁能改。 教材 16.1 分四小节:登录名与数据库用户的区别、创建用户、修改用户、删除用户。 第一小节是根基——这个「两层」没搞清,后面的授权全是乱的。 SQLite 没有这套体系(它只有文件权限),所以这一章的演示是用 Python 复刻的权限模型: 登录、映射、孤儿用户都真跑一遍,复刻的是语义,不是 SQL Server 的实现。 教材原码照贴,标明不执行。
一、两层:先能进服务器,才能进数据库
| 层级 | 对象 | 回答什么问题 | 存在哪 |
|---|---|---|---|
| 服务器级 | 登录名 LOGIN |
你能不能连上这台服务器 | master 库的 sys.server_principals |
| 数据库级 | 用户 USER |
连上之后,你能碰哪个库 | 各库的 sys.database_principals |
登录名管「进门」,用户管「进屋」。 一个人要能查某个库,必须两层都有: 先有登录名(能连服务器),再有映射到该库的用户(能进这个库)。 教材那句话很准:登录名是服务器级的,用户是数据库级的。 所以库被删了,登录名还在(它属于服务器); 登录名被删了,那个库里的用户就变成孤儿——没人能通过它进来。 为什么非要拆两层? 因为一个登录名可以映射到多个库的用户,而且每个库里的权限不一样。 一个人在一个库是只读、在另一个库是管理员,这是常态——一层结构表达不了这件事。
二、建一个能用的账号:两步
CREATE LOGIN u_zhang WITH PASSWORD = 'Abc@12345'; -- 第 1 步:服务器级
USE sales;
CREATE USER u_zhang FOR LOGIN u_zhang; -- 第 2 步:库级映射
GO
复刻模型跑出来的三种登录结果:
| 尝试 | 结果 |
|---|---|
| 登录名正确、密码正确 | 登录成功 |
| 登录名正确、密码错 | 密码错误 |
| 登录名不存在 | 登录名不存在 |
注意后两行:报错是分开的。 SQL Server 真实报错也分开(登录失败 18456 附带状态码),
而很多业务系统会故意统一成「用户名或密码错误」——这是防「枚举账号」,不是偷懒。
密码的要求也值得提一句: SQL Server 默认开「密码策略」,
要求长度、大小写、数字、符号,还会检查「是不是常见弱密码」。
写 PASSWORD = '123456' 直接被拒,这不是 bug。
三、删掉登录名:用户就成了孤儿
| 检查项 | 结果 |
|---|---|
登录名 u_zhang |
已删除 |
库里的用户 u_zhang |
还在,但登录名是空的(孤儿) |
| 还能登录吗 | 登录名不存在 |
孤儿用户不会自己消失,也不会自己恢复。 它占着名字、占着权限,
但没有任何人能通过它进来——因为登录那一层断了。
处理办法两个: 要么把新登录名映射上去(ALTER USER u_zhang WITH LOGIN = u_new),
要么直接删掉用户(DROP USER u_zhang)。
留着不管是最差的选择。 下次有人建同名登录名,
会得到一个「莫名其妙已经有权限」的账号——这种事在交接混乱的库里真会发生,而且很难查。
四、修改与删除:几个常用语句
ALTER LOGIN u_zhang WITH PASSWORD = 'NewPwd@2026'; -- 改密码
ALTER LOGIN u_zhang DISABLE; -- 禁用(登录名还在)
ALTER LOGIN u_zhang ENABLE; -- 启用
DROP LOGIN u_zhang; -- 删登录名
USE sales;
DROP USER u_zhang; -- 删库用户
GO
DISABLE 和 DROP 的区别很重要: 禁用的账号登录名还在、权限还在,
只是连不上(适合「员工离职、待交接」);DROP 是彻底没了,权限也一起消失。
删登录名之前必须先删掉所有库里对应的用户,否则 SQL Server 会报「用户正被使用」。
这不是刁难,是在提醒你「这个账号还在某个库里活着」——
顺着这个报错去查,往往能发现几个忘了清理的库。
还有一种情况更隐蔽: 登录名建了、但没在任何库里建用户,
那这个人能连上服务器,却什么都干不了(连 USE 都进不去)。「能登录」和「能用」是两件事。
五、教材提醒的两件事
第一,别用 sa。 它是系统管理员,权限最大、
名字全世界都知道——爆破它等于直接要库的命。 正确做法是禁用 sa,用自己建的管理账号。
第二,每个应用一个账号,别共用。 共用账号的后果是:
出事查不出是谁干的,要回收权限只能全员改密码。
这两条比任何加密手段都管用。
因为绝大多数数据泄露不是「被高手攻破」,而是「权限给太大、账号共用、离职没回收」——
都是管理问题,不是技术问题。
六、动手 Demo:两把钥匙:登录、映射、孤儿用户
这一段用一个复刻的权限模型,把「登录名 → 库用户」两层走一遍:
建账号、三种登录尝试、删登录名看用户变孤儿。
CREATE LOGIN 那些原码本沙箱不执行,所以先用原码说明语法、再用模型跑结果。
class Sec(object):
"""最小模型:登录(服务器级)→ 用户(库级)。"""
def __init__(self):
self.logins = {} # 登录名 -> {password, enabled}
self.users = {} # 用户名 -> 关联的登录名(None = 孤儿)
def create_login(self, name, pwd):
self.logins[name] = {"password": pwd, "enabled": True}
def login(self, name, pwd):
lg = self.logins.get(name)
if not lg:
return u"登录名不存在"
if lg["password"] != pwd:
return u"密码错误"
return u"登录成功"
def drop_login(self, name):
self.logins.pop(name, None)
for u, l in list(self.users.items()):
if l == name:
self.users[u] = None # 变成孤儿用户
s = Sec()
s.create_login(u"u_zhang", u"Abc@12345")
s.users[u"u_zhang"] = u"u_zhang"
print(s.login(u"u_zhang", u"Abc@12345")) # 登录成功
print(s.login(u"u_zhang", u"abc@12345")) # 密码错误
print(s.login(u"u_wang", u"Abc@12345")) # 登录名不存在
s.drop_login(u"u_zhang")
print(s.users[u"u_zhang"]) # None:孤儿用户
实跑输出:
--------------------------------------------------------------------
用户管理:登录名和数据库用户是两把钥匙
--------------------------------------------------------------------
① 两层:先能进服务器,才能进数据库
--------------------------------------------------------------------
| 层级 | 对象 | 回答什么问题 | 存在哪 |
|---|---|---|---|
| 服务器级 | 登录名 LOGIN | 你能不能连上这台服务器 | master 库的 sys.server_principals |
| 数据库级 | 用户 USER | 连上之后,你能碰哪个库 | 各库的 sys.database_principals |
登录名管「进门」,用户管「进屋」。 一个人要能查某个库,必须两层都有:
先有登录名(能连服务器),再有映射到该库的用户(能进这个库)。
教材里那句话很准:登录名是服务器级的,用户是数据库级的。 库被删了,登录名还在;
登录名被删了,那个库里的用户就变成「孤儿用户」——谁也登不上它,因为它对应的登录名没了。
② 建一个能用的账号:两步
--------------------------------------------------------------------
[教材原码 · 本沙箱不执行]
| CREATE LOGIN u_zhang WITH PASSWORD = 'Abc@12345'; -- 第 1 步:服务器级
| USE sales;
| CREATE USER u_zhang FOR LOGIN u_zhang; -- 第 2 步:库级映射
| GO
下面是复刻模型跑出来的结果(CREATE LOGIN 那两句原码本沙箱不执行):
登录名 u_zhang 已建(密码 Abc@12345),并映射为 sales 库的用户
| 尝试 | 结果 |
|---|---|
| 登录名正确、密码正确 | 登录成功 |
| 登录名正确、密码错 | 密码错误 |
| 登录名不存在 | 登录名不存在 |
注意第二行:密码错了只说「密码错误」,第三行只说「登录名不存在」。
SQL Server 真实的报错也是分开的(18456 附带状态码),
但很多系统会故意统一成「用户名或密码错误」——这是防「枚举账号」,不是偷懒。
③ 删掉登录名:用户就成了孤儿
--------------------------------------------------------------------
| 检查项 | 结果 |
|---|---|
| 登录名 u_zhang | 已删除 |
| 库里的用户 u_zhang | 还在,但登录名是空的(孤儿) |
| 还能登录吗 | 登录名不存在 |
孤儿用户不会自己消失,也不会自己恢复。
它占着名字、占着权限,但没有任何人能通过它进来——因为登录那一层断了。
处理办法有两个: 要么把新登录名映射上去(ALTER USER u_zhang WITH LOGIN = u_new),
要么直接删掉用户(DROP USER u_zhang)。
留着不管是最差的选择,下次有人建同名登录名,会得到一个「莫名其妙已经有权限」的账号。
④ 修改与删除:几个常用语句
--------------------------------------------------------------------
[教材原码 · 本沙箱不执行]
| ALTER LOGIN u_zhang WITH PASSWORD = 'NewPwd@2026'; -- 改密码
| ALTER LOGIN u_zhang DISABLE; -- 禁用(登录名还在)
| ALTER LOGIN u_zhang ENABLE; -- 启用
| DROP LOGIN u_zhang; -- 删登录名
| USE sales;
| DROP USER u_zhang; -- 删库用户
| GO
DISABLE 和 DROP 的区别很重要: 禁用的账号登录名还在、权限还在,
只是连不上(适合「员工离职待交接」);DROP 是彻底没了,权限也一起消失。
删登录名之前必须先删掉所有库里对应的用户,否则 SQL Server 会报「用户正被使用」——
这不是刁难,是在提醒你「这个账号还在某个库里活着」。
⑤ 教材提醒的两件事
--------------------------------------------------------------------
第一,别用 sa。 它是系统管理员,权限最大、名字全世界都知道,爆破它等于直接要库的命。
正确做法是禁用 sa,用自己建的管理账号。
第二,每个应用一个账号,别共用。
共用账号的后果是:出事了查不出是谁干的,要回收权限只能全员改密码。
一个应用一个账号,权限按最小给——这条比任何加密手段都管用。
三种登录结果这一张表值得盯一眼: 密码错和登录名不存在是两种不同的回答。
真实系统里为了让攻击者猜不出「这个账号存不存在」,往往把它们统一成一句话——
这是安全性上的取舍,不是马虎。
drop_login 那个 self.users[u] = None 就是「孤儿用户」的由来。
注意模型里用户没被删、权限也没被清——这正是孤儿用户危险的地方:权限还在,人进不来。
真实 SQL Server 里 DROP LOGIN 有个前置条件:
所有库里对应的用户必须先删掉,否则直接报错。
模型里没做这个限制,是为了让你看清「不删会怎样」。
这段代码一共三十行,但它把「两层钥匙」讲完了。
记住一句话就够:登录名属于服务器,用户属于库;两层都通,才叫「能用」。
30 秒实验
- 把
login()里两个if合并成一句「用户名或密码错误」,跑一遍。 三条尝试会给出同样的回答——这就是「防枚举」的实现方式,也是最常见的做法。 - 给模型加一个
enabled判断(禁用登录),再跑一次正确密码。 应该返回「登录已禁用」——这就是ALTER LOGIN ... DISABLE的效果:密码对也进不来。 - 想一想:一个员工离职,你会立刻
DROP LOGIN还是先DISABLE? 先DISABLE——权限和审计记录都还在,交接完再删; 直接DROP会把「他做过什么」的线索一起抹掉。
常见误区
- 「建了登录名就能查数据了」。
不能。 登录名只让你连上服务器,
还得在目标库里建一个用户(
CREATE USER ... FOR LOGIN)。否则连USE都进不去。 - 「删登录名时用户会跟着删」。 不会。 SQL Server 会先拦住你(报「用户正被使用」); 真删成了,用户会变成孤儿——权限还在、谁也进不来。
- 「一个应用大家共用一个账号省事」。 省的是配置,赔的是排查。 出事查不出是谁、回收权限只能全员改密码、 审计日志里全是同一个名字。
自测三问(点开对答案)
1. 登录名和数据库用户分别属于哪一级?各管什么? —— **登录名是服务器级的,管「能不能连上来」;用户是数据库级的,管「能进哪个库」。** **两层都通,才叫「能用」。** 2. 删除登录名之后,库里原来那个用户会怎样? —— **变成「孤儿用户」**——还在、权限还在,**但没有登录名能对应它,谁也进不来。** **要清理就 `DROP USER`,或者给它映射一个新登录名。** 3. `DISABLE` 和 `DROP LOGIN` 该什么时候用哪个? —— **离职待交接用 `DISABLE`**(权限和审计线索都留着,人进不来);**确认清理干净了再 `DROP`。**下一篇 16.2 讲角色:固定服务器角色(sysadmin、dbcreator)和数据库角色(db_owner、
db_datareader)各是什么,以及为什么「securityadmin 其实接近 sysadmin」。
- ☐ (本篇完,下一篇 16.2 角色管理)