
1. 为什么“加个用户”这件事值得单独写一篇在数据库运维的日常里SQL Server 2008 R2算是一位“老熟人”了。很多企业的内部业务系统、报表平台、老一代的 ERP 或进销存软件后台还稳稳地跑在它上面。这类环境有个共同特点系统不能随便停账号不能随便给权限更不能随便开。于是“新增一个用户并设置访问权限”就成了一个高频、刚需、但又极容易踩坑的操作。我见过太多这样的情况开发同事跑过来说“给我开个账号我连一下库查点数据”结果有人直接把自己的sa密码丢过去或者图省事在数据库上建个登录名就完事权限一股脑给到db_owner。短期看确实省事长期看就是一颗定时炸弹——误删数据、越权访问、审计时说不清楚全是麻烦。所以这个标题看着朴素背后其实是一整套关于登录名Login、数据库用户User、服务器角色、数据库角色、架构Schema与对象级权限的完整知识链条。这篇内容我打算按真实操作的顺序来讲从理解SQL Server那套“两层身份”模型开始到用图形界面和 T-SQL 两种方式建账号再到把权限收窄到“只能读某几张表”这种精细粒度最后把我这些年踩过的坑和排查思路摊开讲。适合刚接触数据库运维的新手照着做也适合有一定基础、但权限管理一直做得比较粗放的同行拿来对照梳理。核心关键词就三个新增用户、访问权限、最小权限原则全文都围绕它们展开。需要先说明一点SQL Server 2008 R2的界面和较新版本有差异网上很多教程是拿 2016 以后的版本截图讲的照着做会对不上。下面涉及界面的部分我按 2008 R2 的实际布局来描述涉及命令的部分则尽量给通用写法方便你在不同环境里迁移。2. 先把身份模型搞清楚登录名和用户不是一回事2.1 两层身份服务器层的“门禁卡”和数据库层的“工牌”很多人第一次接触SQL Server权限会懵是因为它跟 MySQL 那种“一个账号一套权限”的模型不一样。SQL Server把身份拆成了两层我用一个生活化的类比来解释登录名Login相当于你进公司大楼的门禁卡。它存在于服务器实例这一层负责回答“你能不能连上这台数据库服务器”。数据库用户User相当于你进到某个部门后的工牌。它存在于具体的某个数据库内部负责回答“你在这个库里能干什么”。关键点在于门禁卡和工牌是两套东西。你办了门禁卡建了登录名不代表你自动拥有某个部门的工牌数据库用户反过来工牌必须挂靠在一张有效的门禁卡上。这就是为什么经常有人建完登录名用客户端一连报错“无法打开登录所请求的数据库”或者能连上但看不到任何表——因为数据库用户没建或者建了但没做用户映射。提示一个登录名可以在多个数据库里分别映射成不同的用户用户名甚至可以不和登录名相同。但为了后期维护不混乱我强烈建议登录名和数据库用户名保持一致这是多年下来最省心的习惯。2.2 三种常见的身份类型选错后面全是坑在 2008 R2 里新建登录名时你会面临几种身份验证方式的选择这里必须讲清楚因为选错了后面权限怎么配都不对身份类型适用场景关键说明SQL Server 身份验证应用程序连接、跨域访问、临时运维账号需要设置密码账号密码独立于操作系统最灵活Windows 身份验证域环境内的员工、内部系统集成登录依赖操作系统账号免密码安全性高但受域环境约束混合模式服务器同时允许上面两种大多数生产环境的实际配置这里有个容易被忽略的前提服务器本身的身份验证模式。如果实例装的时候选的是“仅 Windows 身份验证”那你新建 SQL 登录名是建不出来的或者建了也连不上。检查方法是在对象资源管理器里右键服务器节点看属性或者执行-- 查看当前服务器的身份验证模式1 表示仅 Windows0 表示混合模式 SELECT SERVERPROPERTY(IsIntegratedSecurityOnly) AS IsWindowsOnly;返回0才说明混合模式已开启。改这个设置需要重启服务实例生产环境务必挑维护窗口操作别在业务高峰动它。2.3 最小权限原则为什么不能一上来就给 db_owner这是整篇内容里我最想强调的一点。db_owner是数据库级别的最高权限角色能建表、删表、改数据、改权限几乎等于这个库的“管理员”。给开发或报表人员开db_owner等于把整个库的生死交出去。正确的思路是按需分配、逐步收窄先想清楚这个人/这个程序到底要做什么是只读查询、还是要写入某几张表、还是需要执行某些存储过程然后只授予对应的权限。SQL Server提供了从服务器角色、数据库角色到对象级权限的完整粒度完全没必要“一刀切给最高权限”。后面第 4 节我会给出“只读某几张表”这种典型场景的完整配置。3. 图形界面实操从建登录名到映射数据库用户3.1 用 SSMS 新建登录名的完整路径打开SQL Server Management Studio简称 SSMS连上目标实例按下面的路径走在左侧对象资源管理器展开服务器节点找到“安全性”文件夹。右键“登录名”选择“新建登录名”。在弹出的窗口里先选身份验证方式。如果是给应用程序用选“SQL Server 身份验证”输入登录名和密码。取消勾选“用户在下次登录时必须更改密码”给程序用的账号勾了会导致连接失败这个细节很多人栽过。切到左侧的“用户映射”页勾选这个登录名要访问的数据库在右侧“数据库角色成员身份”里先给个基础角色比如public或者按需勾db_datareader。点确定保存。这套流程走完登录名和数据库用户就一起建好了。注意第 5 步的“用户映射”是关键它同时完成了“建数据库用户”和“绑定登录名”两件事。如果你只在“常规”页建了登录名没做映射那这个账号连上服务器后是看不到目标库的。3.2 图形界面里那些容易点错的选项界面操作看着简单但有几个地方我见过反复出错默认数据库在“常规”页底部有个“默认数据库”下拉框。如果不设账号连上后默认进master库很多程序连上来执行语句会报“对象不存在”其实是连错库了。给业务账号一定要指定它该去的那个库。强制密码过期给程序账号千万别勾否则密码到期后应用直接连不上半夜告警。“已禁用”状态新建时如果误勾了“登录已禁用”账号建了也白建连不上。排查时先看这个。服务器角色页默认只勾public就够了。除非确实需要别去勾sysadmin、serveradmin这些那是服务器级的高权限。注意图形界面操作虽然直观但不可复现、不好审计。生产环境我建议一律用脚本建账号把脚本存档谁在什么时候开了什么权限一查便知。下面就给脚本写法。3.3 用 T-SQL 建登录名和用户可存档可复现同样的操作用脚本表达出来是这样我按“建登录名 → 建数据库用户 → 授权”三步拆开-- 第一步在服务器层创建登录名SQL Server 身份验证 USE master; GO CREATE LOGIN report_user WITH PASSWORD A_Str0ng_Passw0rd!, DEFAULT_DATABASE SalesDB, CHECK_POLICY ON, CHECK_EXPIRATION OFF; GO -- 第二步在目标数据库内创建对应的数据库用户并绑定登录名 USE SalesDB; GO CREATE USER report_user FOR LOGIN report_user WITH DEFAULT_SCHEMA dbo; GO这里解释几个参数背后的逻辑CHECK_POLICY ON让密码遵循操作系统的密码复杂度策略这是安全基线建议开。CHECK_EXPIRATION OFF对程序账号关掉密码过期避免到期断连对人工账号可以设ON强制定期改密。DEFAULT_SCHEMA dbo指定默认架构。不指定的话用户查询不带架构前缀的表名时可能找不到对象这是新手常遇到的“表明明在却查不到”的原因之一。脚本建完用下面这句验证一下映射关系是否正常-- 查看登录名与数据库用户的映射情况 USE SalesDB; SELECT dp.name AS DBUser, sp.name AS LoginName, dp.type_desc FROM sys.database_principals dp LEFT JOIN sys.server_principals sp ON dp.sid sp.sid WHERE dp.name report_user;如果LoginName那列是NULL说明用户没绑上登录名是个“孤儿用户”得用ALTER USER ... WITH LOGIN ...修复。4. 权限精细化配置从“能连”到“只能读某几张表”4.1 角色授权最省事的粗粒度方案如果需求是“这个账号要能读整个库的所有表”那最省事的方式是把它加进内置的db_datareader角色USE SalesDB; GO ALTER ROLE db_datareader ADD MEMBER report_user; GOSQL Server内置的常用数据库角色有这么几个理解它们能帮你快速选型角色名权限范围典型用途db_datareader可读库内所有用户表报表、查询类账号db_datawriter可增删改库内所有用户表业务写入账号db_ddladmin可建改删表结构等 DDL 操作开发调试账号db_owner库内最高权限仅限库管理员db_denydatareader拒绝读取特殊隔离场景角色授权的好处是简单、好维护坏处是粒度太粗——db_datareader一给整个库所有表都能读包括那些敏感的薪资表、客户信息表。如果库里有敏感数据就得往下走对象级授权。4.2 对象级授权只给某几张表的读权限真实场景里更常见的是“只让这个账号读Orders和Products两张表”。这时候用GRANT精确授权USE SalesDB; GO -- 只授予这两张表的 SELECT 权限 GRANT SELECT ON dbo.Orders TO report_user; GRANT SELECT ON dbo.Products TO report_user; GO如果还要限制到列级别比如只让看订单表的非敏感字段可以这样-- 列级授权只允许读取指定列 GRANT SELECT ON dbo.Orders (OrderID, OrderDate, Amount) TO report_user; GO列级权限在实际中用得不算多因为维护成本高——表结构一改就得同步调整授权。但涉及身份证号、手机号这类敏感字段时它是必要的隔离手段。4.3 拒绝优先DENY 的优先级高于 GRANTSQL Server的权限判定有个重要规则DENY 优先于 GRANT。也就是说哪怕用户通过角色拿到了读权限只要在某个对象上被显式DENY了那就是拒绝。-- 即使 report_user 属于 db_datareader也无法读取 Salary 表 DENY SELECT ON dbo.Salary TO report_user; GO这个特性很有用你可以先用角色给一个宽泛的读权限再用DENY把个别敏感表挖掉比一张张表去GRANT省事得多。但要小心DENY一旦加上排查权限问题时很容易被忽略——明明角色给了权限却读不了八成是某处有DENY。查权限的语句后面会给。4.4 存储过程与执行权限程序账号的常见需求很多业务程序不直接查表而是通过存储过程访问数据。这种情况下正确的做法是只授予存储过程的EXECUTE权限而不给底层表的直接访问权-- 只允许执行指定存储过程 GRANT EXECUTE ON dbo.usp_GetOrderReport TO report_user; GO这样做的好处是权限收口程序只能通过存储过程规定的逻辑访问数据绕不过业务规则也看不到底层表结构。这是比直接给表权限更安全的做法我在给外部系统开账号时基本都用这个套路。5. 验证与排查权限配完了怎么确认真的生效5.1 用 EXECUTE AS 模拟用户身份验证配完权限最忌讳“想当然”。最靠谱的验证方式是模拟目标用户身份去执行操作看能不能成功-- 模拟 report_user 身份执行查询 EXECUTE AS USER report_user; SELECT TOP 10 * FROM dbo.Orders; -- 应该成功 SELECT TOP 10 * FROM dbo.Salary; -- 应该失败无权限 REVERT; -- 恢复原身份EXECUTE AS是排查权限问题的利器不用真的拿账号密码去登录直接在会话里切换身份测试。测完记得REVERT切回来否则后续操作都以模拟身份执行容易出乱子。5.2 查权限的几条实用语句权限配得多了时间一长自己都记不清谁有什么权限。下面几条查询语句建议收藏-- 查看某个数据库用户被授予的对象级权限 USE SalesDB; SELECT pr.name AS UserName, ob.name AS ObjectName, p.permission_name, p.state_desc FROM sys.database_permissions p JOIN sys.database_principals pr ON p.grantee_principal_id pr.principal_id LEFT JOIN sys.objects ob ON p.major_id ob.object_id WHERE pr.name report_user;-- 查看某个用户属于哪些数据库角色 USE SalesDB; SELECT r.name AS RoleName, m.name AS MemberName FROM sys.database_role_members rm JOIN sys.database_principals r ON rm.role_principal_id r.principal_id JOIN sys.database_principals m ON rm.member_principal_id m.principal_id WHERE m.name report_user;-- 查看服务器级角色成员排查是否被误加了高权限 SELECT r.name AS ServerRole, m.name AS MemberName FROM sys.server_role_members srm JOIN sys.server_principals r ON srm.role_principal_id r.principal_id JOIN sys.server_principals m ON srm.member_principal_id m.principal_id;5.3 常见报错与排查速查表实际操作中遇到的报错我整理成一张表方便对照排查报错/现象可能原因解决方向“登录失败用户未关联”登录名建了但没映射数据库用户检查用户映射补建数据库用户“无法打开登录所请求的数据库”默认数据库不存在或无权限检查默认库设置确认用户已映射到该库能连上但看不到任何表用户无对应架构或对象权限检查默认架构、角色成员、对象级 GRANT明明有角色权限却读不了存在 DENY 或对象属主不同查sys.database_permissions找 DENY程序连接报密码错误密码策略/过期/账号被禁用检查CHECK_EXPIRATION、登录状态查询报“对象名无效”默认架构不对或连错库确认DEFAULT_SCHEMA和当前库提示排查权限问题时顺序建议是“先看能不能连登录名→ 再看能不能进库用户映射→ 最后看能不能操作对象对象权限”一层层往下查比乱试快得多。6. 几个我踩过坑才记住的经验6.1 孤儿用户和孤立登录名是迁移后的高发问题数据库从一台服务器备份恢复到另一台或者做实例迁移时经常出现“用户还在、但登录名对不上”的情况这就是孤儿用户。原因是数据库用户内部存的是登录名的 SID换了服务器 SID 变了就对不上了。修复方法是重新绑定USE SalesDB; GO ALTER USER report_user WITH LOGIN report_user; GO迁移后第一件事就是跑一遍孤儿用户检查别等业务报错了才发现。检查语句USE SalesDB; SELECT dp.name AS OrphanUser FROM sys.database_principals dp LEFT JOIN sys.server_principals sp ON dp.sid sp.sid WHERE dp.type IN (S,U) AND sp.sid IS NULL;6.2 给程序账号的密码别用会过期的策略这条前面提过但值得单独强调。给应用程序用的账号CHECK_EXPIRATION一定要关。我见过一个系统半夜全线告警查了半天是数据库账号密码到期应用连不上。人工账号可以强制过期程序账号不行。另外密码本身要够复杂但别用带特殊字符导致连接字符串转义出问题的组合比如密码里有分号、引号写进配置文件时容易出错。6.3 权限变更要留痕脚本比鼠标可靠图形界面点几下很爽但事后没人说得清到底改了什么。我的习惯是所有生产环境的账号和权限变更一律走脚本脚本进版本库。建账号、授权、回收权限每一步都有对应的 SQL 文件出问题能回滚审计时能拿出记录。这个习惯在出过一次“不知道谁把某账号提成了 db_owner”的事故后就再也没断过。6.4 定期做权限巡检别让权限只增不减账号开出去容易收回来难。人员离职、项目下线、临时需求结束对应的账号和权限往往没人清理越积越多。建议每季度做一次权限巡检列出所有数据库用户及其角色成员对照实际需求核对一遍该禁用的禁用、该降权的降权。巡检脚本可以直接用第 5.2 节那几条查询改一改导出成清单人工过一遍。这件事枯燥但它是数据库安全里性价比最高的动作之一。6.5 别忘了 public 角色这个“隐形底座”每个数据库用户默认都属于public角色而public角色默认拥有一些基础权限比如查看部分系统视图。有些环境为了“方便”直接给public角色授了业务表的读权限结果等于所有能连上这个库的账号都能读。这是个隐蔽的权限放大问题配置对象权限时一定要确认public角色没有被过度授权。查一下USE SalesDB; SELECT p.permission_name, p.state_desc, ob.name AS ObjectName FROM sys.database_permissions p JOIN sys.database_principals pr ON p.grantee_principal_id pr.principal_id LEFT JOIN sys.objects ob ON p.major_id ob.object_id WHERE pr.name public AND p.major_id 0;如果查出来public对业务表有SELECT那基本可以判定权限模型有问题得重新梳理。7. 收个尾这套流程的复用价值把上面这套东西串起来看其实就是一个可复用的“账号开通标准动作”确认身份验证模式 → 建登录名指定默认库、关程序账号过期→ 映射数据库用户统一命名、指定架构→ 按最小权限原则授角色或对象权限 → 用 EXECUTE AS 验证 → 脚本存档留痕。这套动作在SQL Server 2008 R2上跑通换到更新的版本上逻辑也基本一致只是界面细节有差异。我个人在实际操作中的体会是权限这件事最怕的不是技术难而是“图省事”。一次图省事给了db_owner后面可能要花十倍精力去收拾。宁可开账号时多花五分钟想清楚权限边界也别给未来埋雷。另外SQL Server 2008 R2毕竟是老版本如果环境允许逐步往受支持的新版本迁移是更根本的解法但迁移之前把现有环境的账号权限梳理干净本身就是迁移前必做的功课之一。