如果你要维护一个两百人以上的AD域环境,你大概率经历过这种周一早上:工单里几条“密码过期”的求助,两条“新员工入职”的开户申请,偶尔还有一条“账号被锁了来个人解锁”。我刚接手域管理那阵子,处理这些东西全靠AD用户和计算机,右键、重置密码、勾选下次登录修改,再手动填部门、填组、填到期时间,动作熟练得像流水线,但效率低得离谱。真正逼我改变的是某次一天入职三十人,我在工位上坐了快一上午才建完,还漏掉了一个人的组关系。从那天起,我把日常的AD域账户管理操作全部梳理了一遍,能用脚本和流程解决的绝不再手动点一遍。这篇文章就是这套“简单管理”方案的完整复盘,适合正在维护域环境、每天被重复工单压着的同行参考。
1. 从周一早上的工单堆说起:域账户管理到底麻烦在哪
1.1 重复操作才是最大的隐形工作量
单看一次操作,域账户管理里几乎没有难事。新员工开户,无非是新建用户、设初始密码、加组;密码过期,重置一下再让用户改;离职清理,禁用账号、移出对应组织单元,再核对一下特权组。每一项单独拎出来,五分钟内都能做完,可一旦乘以日常工单量,情况就不一样了。
我当时所在的部门,域内实际启用账号四百多个,每周平均新增十来个工单,旺季一天入职三十人也不是没出现过。表面上看,麻烦的是“操作多”,但真正吃掉时间的,是同一套动作被反复执行,而且每次都要靠人脑回忆:这个人该放到哪个OU?该加哪几个组?初始密码是哪个版本?到期日设到什么时候?这些问题没有固定答案的时候,操作一次就要查一次,查的过程中还可能点到错误的OU。对比之后你就会发现,所谓“简单管理”,核心不是某个炫酷工具,而是把重复的、有规律的动作全部模板化、批量化和流程化。
1.2 把操作拆成“能脚本化”和“必须人工判断”两类
我把日常工单分成了两类。
能脚本化的场景包括:批量开户、批量调岗属性修改、密码重置与解锁、定期离职清理、闲置账户盘点。这些操作的共同特点是字段固定、规则清楚,输入参数只要给全,结果可预期,完全可以用PowerShell加CSV处理。
必须人工判断的场景包括:特殊权限申请、重复锁定的根因分析、跨部门的复杂调动、以及对账号归属有争议的清理。这类事情涉及业务判断,不适合一刀切自动化。
分类之后,我给自己定了一条规矩:凡是上个月执行过两次以上的重复操作,这个月就必须写成脚本,否则就是自己在给自己找麻烦。下面几节,我会按日常频率最高的几个场景,把脚本和思路完整展开。
2. 把开户从半小时压缩到十秒:CSV模板与批量脚本
2.1 先把CSV模板做扎实
批量开户的第一步不是写脚本,而是做一张好用的CSV模板。我的模板放在D:\scripts\new_users.csv,列固定为:登录名,姓名,部门,职位,到期日。登录名我用工号或拼音全称,规则定了以后,账号命名就再也不用临时拍脑袋了。部门这一列直接决定账号会落到哪个OU,所以写之前要确认一下目标OU路径真实存在。
如果你现在还没规划好OU结构,建议先把“按部门建OU”这件事补上。路径写得越规整,后面所有脚本越简单。我这边的主路径是OU=员工,DC=corp,DC=local,部门OU挂在员工OU下面,因此新建账号的Path参数可以直接拼出来。如果哪天真有人需要在别的OU建账号,脚本里单独给一个“指定OU”的列就好,这样模板的通用性也会好很多。
2.2 开户脚本解析
我的批量开户脚本大致是这个样子:
Import-Module ActiveDirectory $users = Import-Csv -Path "D:\scripts\new_users.csv" -Encoding UTF8 $initPwd = ConvertTo-SecureString -String "Init@12345" -AsPlainText -Force foreach ($u in $users) { $sam = $u.登录名 $upn = "$sam@corp.local" $newUser = New-ADUser ` -SamAccountName $sam ` -UserPrincipalName $upn ` -Name $u.姓名 ` -DisplayName $u.姓名 ` -Department $u.部门 ` -Title $u.职位 ` -Path "OU=$($u.部门),OU=员工,DC=corp,DC=local" ` -AccountPassword $initPwd ` -Enabled $true ` -ChangePasswordAtLogon $true ` -PassThru if ($u.到期日) { Set-ADAccountExpiration -Identity $newUser -DateTime $u.到期日 } }这里有几个容易被忽略的点。
-ChangePasswordAtLogon $true是必须的,它让用户第一次登录就被系统强制改密码,初始密码就算泄露,用过一次也就作废了。-PassThru也很关键,没有它,New-ADUser执行完不会把创建好的用户对象返回给变量,后面的Set-ADAccountExpiration就无法引用这个新账号,只能再查一次。ConvertTo-SecureString看起来绕,但AD模块里所有密码参数都要求SecureString类型,这是绕不开的。
执行前记得做两件事:一是确认当前PowerShell会话的账号有权限在目标OU里创建用户;二是先给CSV里只留一行测试数据跑一遍。我见过太多同事拿着两百行的CSV直接跑到生产环境,报错之后才发现是有个登录名重复,收尾非常狼狈。
2.3 顺手搞定批量调岗与加组
开户只是前半段,员工调岗、批量加组同样是高频工作。这类需求我很少用ADUC手动点,而是让HR或行政按固定格式交一份CSV,我这边用脚本一次性处理:
$users = Import-Csv -Path "D:\scripts\update_users.csv" -Encoding UTF8 foreach ($u in $users) { Set-ADUser -Identity $u.登录名 -Department $u.部门 -Title $u.职位 if ($u.组) { Add-ADGroupMember -Identity $u.组 -Members $u.登录名 } }这个脚本的注意点在于-Identity参数很宽容,可以传登录名、DN、GUID,但正因为宽容,CSV里一旦有重复或拼错的名字,你就很难从报错信息里快速定位是第几行出了问题。所以我的习惯是执行前先对CSV做一次去重检查,再对“即将修改谁”有个预期清单,避免跑完了才发现把某个不该动的人属性改掉了。
3. 密码重置与账户解锁:把高频工单变成半自动流程
3.1 密码操作要区分场景
密码类工单看着都是“密码不行了”,但原因和对应处理其实是不同的。我按现象列过一个对照表:
| 场景 | 典型现象 | 处理方式 |
|---|---|---|
| 忘记密码 | 登录失败,密码未过期 | 重置密码,并强制下次登录修改 |
| 密码过期 | 登录时提示密码已过期 | 重置密码,或让用户走自助改密入口 |
| 多次输错被锁定 | 提示账户被锁定 | 解锁,同时查锁定根因 |
| 临时账号到期 | 登录提示账号已过期 | 重新设置到期时间或做延期 |
这里要理解AD里的一个关键机制:用户密码被重置后,默认还带有一个“下次登录是否必须修改”状态。AD系统里这个状态由pwdLastSet属性控制,PowerShell能直接操作它:
# 强制该用户下次登录修改密码 Set-ADUser -Identity "zhangsan" -Replace @{pwdLastSet = 0} # 让密码永不过期 Set-ADUser -Identity "zhangsan" -Replace @{pwdLastSet = -1}依赖这个属性,很多看似需要反复手工做的“改密套路”都可以封装进一条脚本。
3.2 重置密码脚本与强制下次改密
日常用得最频繁的密码重置脚本,我写成了下面这样:
$identity = Read-Host "请输入需要重置密码的登录名" $newPassword = Read-Host "请输入新密码" -AsSecureString Set-ADAccountPassword -Identity $identity -NewPassword $newPassword -Reset Set-ADUser -Identity $identity -ChangePasswordAtLogon $true Unlock-ADAccount -Identity $identity三条命令的顺序是有讲究的。Set-ADAccountPassword后面的-Reset表示这是管理员显式重置,而不是走“修改密码”流程,旧密码是否知晓无所谓。设为ChangePasswordAtLogon $true,是让用户下次登录必须改成只有自己知道的密码。最后再顺手执行一次Unlock-ADAccount,避免用户拿着新密码去登录,却被锁定状态卡住的尴尬局面。
实际工单里经常出现的情况是,用户密码已经过期,重置完新密码之后系统仍提示需要改密,但只要下面的强制改密属性已经设置,通常都能正常走完流程。如果反复出现“重置后无法登录”的情况,优先检查CSV里登录名是否与用户实际输入的账号大小写或前后空格不一致,这个坑比AD配置本身出现的概率高得多。
3.3 锁定账户:先解锁,更要找根因
账户锁定这件事,表面上一条Unlock-ADAccount就结束了,但只做解锁不查原因,通常一周内还会被同样的工单叼回来。常见的锁定来源有三个:用户连续输错密码;手机邮件客户端或无线认证缓存了旧密码,每过一会儿就自动验证一次;第三方系统用旧密码调用域认证,导致反复锁定同一个账号。
排查时可以先用脚本把当前锁定的账号都列出来:
Search-ADAccount -LockedOut | Format-Table Name, SamAccountName, LockedOut, LockoutTime如果需要更精确的源头,就要去看域控安全日志里的事件ID 4740(账户被锁定),在中小规模域环境里,锁定事件通常会记录在被锁账号所在域的任意域控上,重点查PDC主控最容易定位。我的处理习惯是:首次锁定,解锁并重置密码;两周内第二次锁定,就不再盲目解锁,而是顺着事件日志找是哪台机器、哪个系统一直在拿旧密码撞,该改计划任务的改计划任务,该更新移动端配置的更新配置,否则解锁再多次也是白费。
4. 离职与闲置账户:生命周期管理里最容易被漏掉的后半段
4.1 离职处理三步走
新员工开户做得再多,如果离职账户处理得敷衍,安全隐患一样会膨胀。我给自己定的离职处理标准动作是三步:禁用账号、把账号移到“离职用户”OU、移除特权组成员关系。
$sam = "zhangsan" Disable-ADAccount -Identity $sam $user = Get-ADUser -Identity $sam Move-ADObject -Identity $user.DistinguishedName -TargetPath "OU=离职用户,DC=corp,DC=local" $groups = Get-ADPrincipalGroupMembership -Identity $sam | Where-Object { $_.Name -ne "Domain Users" } foreach ($g in $groups) { Remove-ADGroupMember -Identity $g -Members $sam -Confirm:$false }禁用账号是为了让登录立即失效,这是安全底线。移到“离职用户”OU,一方面避免这个账号继续接收正常业务OU的组策略,另一方面也让后续审计时能一眼看出哪些人是离职状态。移除特权组成员关系则是对付那种“表面离职、后台还是某个管理组成员”的典型风险。第三步里注意保留Domain Users,这是域内所有用户的基础组,强行移除会出各种奇怪问题。
不直接删账号,是因为禁用后的账号还保留了SID和历史权限记录,万一涉及审计、邮箱留存或者员工重新入职,都能派上用场。真要清理,也应该走单独的周期任务,而不是离职当天就删。
4.2 闲置账户的盘点与自动禁用
除了明确的离职流程,域里最隐蔽的风险是那些“人已经不在,但账号还活着”的闲置账户。我每季度做一次盘点,用最后登录时间筛出长期不用的启用账号:
$cutoff = (Get-Date).AddDays(-90) Get-ADUser -Filter "Enabled -eq 'True'" -Properties LastLogonDate, SamAccountName | Where-Object { $_.LastLogonDate -lt $cutoff } | Sort-Object LastLogonDate | Select-Object Name, SamAccountName, LastLogonDate | Export-Csv "D:\scripts\inactive_90d.csv" -NoTypeInformation -Encoding UTF8拿到盘点清单后,我不会直接批量禁用,而是先把清单发给各部门负责人确认一遍,排除掉该账号可能只是“没人用但不是不该存在”的服务账号或共享账号。确认后再执行禁用。这里有个技术细节:LastLogonDate对应的是msDS-LastLogonTimeStamp属性,它的更新是有时间戳同步容差的,对“精确到哪一天没登录”不友好,但对季度级别的闲置判断完全够用。真想精确到分钟级,就需要逐个域控查lastLogon再取最大值,日常管理没有必要上到这个精度。
5. 用报表和委托权限把日常负担降下来
5.1 账户全景报表:让数据帮你看问题
不再靠脑子记状态,是“简单管理”的另一块基石。我会每周生成一份域账户全景报表,格式简洁但字段齐全:
Get-ADUser -Filter * -Properties Department, Title, LastLogonDate, AccountExpirationDate, pwdLastSet | Select-Object SamAccountName, Name, Department, Title, LastLogonDate, AccountExpirationDate, @{n='密码最后一次修改';e={[DateTime]::FromFileTime($_.pwdLastSet)}} | Export-Csv "D:\scripts\ad_report_$(Get-Date -Format yyyyMMdd).csv" -NoTypeInformation -Encoding UTF8密码到期时间也可以提前算出来,利用的是AD里计算属性msDS-UserPasswordExpiryTimeComputed,不需要自己猜密码策略:
Get-ADUser -Filter * -Properties "msDS-UserPasswordExpiryTimeComputed" | Select-Object Name, SamAccountName, @{n='密码到期时间';e={[DateTime]::FromFileTime($_."msDS-UserPasswordExpiryTimeComputed")}} | Sort-Object 密码到期时间 | Export-Csv "D:\scripts\pwd_expiry_$(Get-Date -Format yyyyMMdd).csv" -NoTypeInformation -Encoding UTF8这两份报表我放进任务计划程序里,每周自动跑一次,结果导出到共享目录,管理员们按需查看。每周扫一眼“未来两周内有三百个账号同时到期”这种数据,比等用户集体来电求救要有用得多。
5.2 把高频权限委托给一线支持
脚本化虽然提效,但还有一个现实问题:一线支持人员每天要处理大量密码重置,总不能每次都借用管员账号跑脚本。解决思路是AD的委派控制,而不是直接给Domain Admin。
打开AD用户和计算机,在“员工”这个OU上右键选“委派控制”,把“重置用户密码并强制在下一次登录时更改密码”这类权限授给一线支持组。委派完成后,这些同事在自己的电脑上装好RSAT工具,就能用同样的Set-ADAccountPassword和Unlock-ADAccount命令,而且权限范围只限被委派的OU,误操作不会波及整个域。用委派而不是管理员的另一个好处是,所有操作都会走普通安全日志,出了问题时,审计线索更清爽。
6. 踩坑记录与批量操作前必读的安全习惯
6.1 编码、命名与“看起来正常”的脚本
最容易翻车的不是脚本本身,而是CSV文件编码。Excel保存CSV时,如果直接选“CSV(逗号分隔)”而不是“CSV UTF-8”,文件会以本地ANSI编码保存,中文导入PowerShell 5.1后常常变成乱码,账号建出来姓名全是问号。我现在的固定做法是:Excel导出时选“CSV UTF-8”,PowerShell这边Import-Csv和Export-Csv都显式带-Encoding UTF8。反过来,当脚本生成的报表用Excel打开乱码时,多半是导出编码没带BOM,把-Encoding UTF8换成带BOM的写法就能解决。
登录名命名规则也要提前钉死。我见过CSV里登录名带着中文、空格甚至特殊字符,导致New-ADUser直接报错的。域账号的登录名最好统一用工号或者拼音,短横线和下划线可以用,空格一定不要出现。别忘了同一个OU下Name列(也就是CN)必须唯一,如果公司有两个“王伟”,开户前就得先确认CSV里已经做了区分。
6.2 批量操作的三条铁律
经验堆出来的三条规矩,写在这里当清单。
第一条,先单行后批量。任何新脚本第一次跑,CSV里只留一行,确认对象正确、属性无误之后,再放开跑全量。第二条,操作前先出快照。批量改属性、批量禁用之前,把目标账号的当前状态导出一份CSV,万一改错了,至少知道回滚到哪个状态。第三条,谨慎使用-Confirm:$false。它确实能让批量脚本免去反复确认的打扰,但也意味着脚本有任何逻辑漏洞,都会直接作用到全部目标上。我通常只在离职清理这种明确单向操作的脚本里用它,其他场合宁愿多看一眼确认提示。
脚本的统一管理也值得多说一句。我把所有AD相关脚本放在同一个目录,命名带上场景和日期,例如bulk_new_users_20250110.ps1、reset_pwd_20250110.ps1。半年后回看,或者交给接手的人,都能快速知道这是干什么用的、什么时候改的,避免“脚本散落四处,运行前都得靠猜”的老毛病。
这套管理方式我坚持了一年多,现在工作日早上的AD相关工单基本在十分钟内清完,批量开户三十人的场景也能在喝杯水的功夫里跑完脚本并核对好输出。最后补一个经验:AD域账户管理这件事,核心技术含量其实不高,真正拉开差距的是你能不能把重复劳动沉淀成模板和脚本,再配合一点必要的审计习惯。把这套逻辑理顺了,你会发现“简单管理”并不是一句口号,而是每天真实的工作状态。