Python脚本实现自动化管理Windows域

📅 发布时间:2026/10/12 6:17:26
Python脚本实现自动化管理Windows域
前言域Active Directory环境里靠「打开图形界面一个个点」来盘点账号几百上千人的组织根本做不完。用脚本做只读的查询与导出才是运维自动化里性价比最高的一档需求把域内用户、安全组、账号状态是否禁用、密码是否过期、多久没登录批量取出来落成报表比手工翻管理控制台快得多也更容易复现。必须先把边界说清楚因为这类脚本一旦越界就是安全事故。运行这类脚本需要域管理员或至少被委派了对应读取权限的管理员的明确授权且只能在你确实拥有管理权的目录域、组织单元上操作。本文只演示查询与只读导出账号的创建、删除、改密码、加组这类写操作只说明它存在不提供成品脚本——写操作一旦参数写错可能批量锁死或误删账号风险远高于收益应当走经过审批的专门工具。常见误解是「能连上域就能随便读」。实际恰恰相反域是一个有严格访问控制的目录服务读哪些属性、能读哪些组织单元都取决于你的账号被授予的权限。脚本拿到的权限不会超过你账号本身的权限这点要有清醒认识。本文以 Python 3 为基准。涉及第三方库ldap3、pywin32时只讲用法思路具体 API 以各库官方文档为准不凭印象拼参数。一、先分清「读」和「写」把需求先分类是这类脚本安全的第一道闸门。操作类型例子本文立场只读查询列出所有用户、查某用户所属组演示只读导出导出账号状态报表到 CSV演示写操作新建用户、禁用账号、改组成员只说明存在不给成品区分标准很简单会不会改变目录里的数据。只读查询无论跑多少次结果都一样最坏情况是把服务查慢写操作则可能不可逆。因此一个安全的实践是给这类脚本专门申请一个只读账号即使脚本被误用破坏面也被限制住。二、连接与认证的基本概念Python 访问域有两条常见路线。一条是LDAP 路线用ldap3这类库。Active Directory 本身就是一个 LDAP 服务器你用 LDAP 协议去查它。它的好处是跨平台、概念清晰连接、基路径、过滤器、属性。# 适用于 Python 3.8需先 pip install ldap3# 具体参数与返回值以 ldap3 官方文档为准import osfrom ldap3 import Server, Connection, ALLdef open_connection(host, base_dn):user os.environ.get(AD_USER) # 形如 userexample.localpassword os.environ.get(AD_PASSWORD)if not user or not password:raise RuntimeError(请通过环境变量提供域账号不要把口令写进代码)server Server(host, get_infoALL)conn Connection(server, useruser, passwordpassword, auto_bindTrue)return conn这段里有两条硬规矩。第一凭据只从环境变量或密钥管理读取绝不硬编码也不写进任何日志——域账号泄露的后果比普通网站账号严重得多。第二连接对象用完要unbind()释放长连接不要一直挂着。另一条是Win32 路线用pywin32提供的 ADSI/COM 接口在 Windows 上直接和目录服务对话。它更贴近 Windows 原生但绑定平台且 API 形态和 LDAP 完全不同。选择哪条取决于你的运行环境和团队习惯本文的示例统一走 LDAP 路线。三、只读查询把用户和组拉出来LDAP 查询的三要素是基路径base DN、过滤器filter、要取的属性attributes。基路径决定「从哪个组织单元往下找」过滤器决定「找什么样的条目」。# 适用于 Python 3.8基于 ldap3API 细节以官方文档为准def list_users(conn, base_dn, limit500):只读列出用户账号的部分属性返回列表conn.search(search_basebase_dn,search_filter((objectClassuser)(objectCategoryperson)),attributes[sAMAccountName, displayName, userAccountControl],size_limitlimit,)rows []for entry in conn.entries:rows.append({账号: str(entry.sAMAccountName),显示名: str(entry.displayName),状态原始值: str(entry.userAccountControl),})return rows几个点要注意。objectCategoryperson是为了把「用户」和「计算机账户」区分开——在 AD 里计算机账户也带objectClassuser不加这条过滤会把一堆机器账号也捞出来这是很常见的困惑来源。size_limit用来给查询封顶避免一次性拉几万条把服务器拖慢分页取数要用库提供的分页机制具体方法以其官方文档为准。导出报表就是把上面的行写进 CSV务必显式指定encodingutf-8-sig否则用 Excel 打开会中文乱码。# 适用于 Python 3.8import csvdef export_rows(rows, out_path):if not rows:return out_pathwith open(out_path, w, encodingutf-8-sig, newline) as f:writer csv.DictWriter(f, fieldnameslist(rows[0].keys()))writer.writeheader()writer.writerows(rows)return out_path四、只读检查账号状态「批量检查账号状态」是另一类高频只读需求哪些账号被禁用、哪些密码长期未改、哪些很久没登录。判断依据是属性值而不是靠猜。userAccountControl是一个位标志bit flag字段里面若干位分别表示「账号被禁用」「密码永不过期」等状态。解析时要按位判断而不是拿它去和某个数字做相等比较——因为每个账号的其他位可能不同直接比数字几乎永远不相等。# 适用于 Python 3.8def is_disabled(account_control_value):userAccountControl 的 0x2 位表示账号被禁用。具体位定义以微软官方文档为准。try:flag int(str(account_control_value))except (TypeError, ValueError):return None # 解析不了就返回未知别硬猜return bool(flag 0x2)def summarize(rows):disabled [r for r in rows if is_disabled(r[状态原始值]) is True]unknown [r for r in rows if is_disabled(r[状态原始值]) is None]return {总数: len(rows),已禁用: len(disabled),状态未知: len(unknown),}这里体现了一个原则解析不出来的项要如实标为「未知」不要默认成「正常」。报表的价值在于准确把未知混进「正常」盘点结论就不可信了。五、写操作只提存在性新建用户、禁用账号、重置密码、加组成员在 AD 里都有对应的写操作库也提供了相应的方法。但本文不展开写法原因在开门见山处已说过一是它们改变目录数据、可能不可逆二是批量写一旦匹配条件写错例如过滤器多匹配了几个账号会造成面状影响。如果确实要自动化写操作正确做法是走组织既定的变更流程在测试目录先行验证、对操作范围做最小化限制、保留操作前后快照、可回滚。这些属于流程设计不是一段示例代码能覆盖的。常见坑点❌ 把域管理员口令硬编码进脚本或图省事发到群里共享。✅ 凭据只从环境变量或密钥管理读取脚本文件本身不含任何口令。❌ 查询时不加objectCategoryperson把计算机账户也当成用户导了出来。✅ AD 里计算机账户同样带objectClassuser务必按objectCategory过滤。❌ 用userAccountControl 512判断账号是否正常。✅ 它是位标志字段要按位判断如flag 0x2不能整体比较。❌ 不设size_limit、不分页一次把整域几万条用户全拉下来。✅ 给查询封顶并分页取数避免把目录服务器查慢。❌ 导出 CSV 不写encoding用 Excel 打开全是乱码。✅ 写encodingutf-8-sig让 Excel 正确识别编码。❌ 把解析失败的账号状态默默当成「正常」报表结论失真。✅ 解析不了就标记为「未知」让异常显式暴露。❌ 拿一个高权限账号到处乱用来跑脚本出事时无法界定影响范围。✅ 为只读脚本单独申请只读账号把权限最小化。❌ 以为「能连上域就能读所有组织单元」实际上权限由委派决定。✅ 先确认你账号被授予的读取范围只在有权限的目录上操作。总结需求做法底线盘点用户/组LDAP 只读查询 CSV 导出只读账号、限条数检查账号状态按位解析userAccountControl未知要如实标注凭据管理环境变量/密钥管理绝不硬编码账号增删改走审批后的专门工具本文不给成品用脚本管理 Windows 域能力越大越要克制。把「只读查询、只读导出」这一半做扎实已经能解决绝大多数盘点与审计需求凡是会改变目录数据的写操作交给有审批、有回滚的流程去做而不是交给一段来路不明的脚本。记住两条铁律权限最小化、凭据绝不进代码。