ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

服务账号+目录接口:多网域用户批量管理自动化实战

服务账号+目录接口:多网域用户批量管理自动化实战

1. 从手动拉表到接口自动化:为什么我决定用服务账号接管用户管理

刚接手组织内账号管理工作那会儿,我每天的状态可以用四个字概括:复制粘贴。新同事入职,我要手动建账号、配群组、设权限;有人离职,我又要挨个去关服务、转文件、留归档。一个几十人的小团队还能扛,一旦组织规模上百、还分了好几个业务域,手动操作就彻底崩了——漏建、错配、权限残留,几乎每周都在上演。

真正让我下决心改造的,是一次季度审计。审计方要一份"所有域下全部账号的最近登录与权限状态"清单,我花了整整两天在后台里翻页、导出、比对,最后还是被指出有两处数据对不上。那一刻我意识到:账号管理这件事,靠人肉是管不住的,必须交给接口。

这篇内容就是把我后来跑通的一套方案完整拆开讲。核心思路是:在管理后台里创建一个服务账号,给它授予必要的目录管理权限,然后通过目录接口去批量读写组织内所有网域的用户。它适合的对象很明确——手里管着多个网域、被重复性账号操作折磨、又不想引入重型第三方系统的运维或IT支持人员。哪怕你之前没怎么写过代码,只要跟着把授权链路走通,后面就是调接口的事。

先说清楚它能干什么:批量创建与停用账号、批量修改组织单位归属、批量重置密码、批量查询登录与权限信息、把结果直接落成表格或写回内部系统。一句话,凡是后台里能点出来的操作,接口基本都能做,而且能一次对全部网域生效。

2. 服务账号到底解决了什么问题:和普通账号的本质区别

2.1 普通管理员账号的三大硬伤

很多人第一反应是:我直接用超级管理员账号调接口不就行了?我一开始也这么想,实测下来有三个绕不过去的坑。

第一是凭证绑人。普通账号的登录态和某个人绑定,人一离职、密码一改、二次验证一开,脚本立刻失效。你不可能把超管密码硬编码进脚本里,那等于把整个组织的钥匙贴在公告栏上。

第二是权限过大且不可控。超管账号是"全有"权限,脚本一旦被误用或泄露,影响面是整个组织。而实际业务往往只需要"读用户目录"或"改用户属性"这一小块能力,用超管属于杀鸡用牛刀,还容易伤到自己。

第三是审计困难。用个人账号做的操作,日志里记的是"某某人",你很难区分这是人工操作还是脚本行为。出了问题追溯起来,责任边界模糊。

2.2 服务账号的运作逻辑:一把只开指定门的钥匙

服务账号的本质,是一个不代表任何真人、专供程序调用的身份。它没有密码登录界面,靠的是一对密钥(公钥存在平台侧,私钥在你手里)来证明"我是我"。你可以把它理解成一把门禁卡:卡本身不认人,只认权限配置——你给它开哪扇门,它就只能进哪扇门。

这套机制带来三个直接好处。一是凭证与人员解耦,密钥文件放在受控的服务器或密钥管理服务里,人员流动不影响脚本运行。二是权限最小化,你可以只授予"目录读写"这一项,其他能力一概不给。三是行为可审计,所有通过服务账号发起的操作,日志里都明确标记为程序身份,人工与自动一目了然。

2.3 一个容易被忽略的前提:域级授权范围

这里有个新手最容易栽的点。服务账号创建出来是"全局"的,但你要操作的是"某个具体网域"下的用户。这两者之间需要一次域级授权委托——也就是告诉平台:"这个服务账号,被允许代表本域去访问目录接口。"

如果跳过这一步,你会遇到一个非常迷惑的报错:接口能连通、鉴权也过了,但一查用户就返回空列表或者权限拒绝。很多人以为是接口写错了,其实是授权范围没配。我后面会专门用一节讲这个坑怎么排查。

3. 动手前的准备:把授权链路一次性铺平

3.1 创建服务账号并拿到密钥文件

进入管理控制台,找到服务账号相关入口,新建一个账号,起个能看懂的名字,比如dir-sync-bot,描述写清楚用途。创建完成后,进入它的密钥管理,新建一对密钥,格式选通用的 JSON。这个 JSON 文件只会下载一次,关掉页面就再也拿不到了,所以务必第一时间存到安全位置。

我一般会把它放在服务器的受限目录里,权限设成只有运行脚本的那个系统用户可读,比如chmod 600。千万别图省事丢进代码仓库,哪怕私有仓库也不行——一旦仓库权限配置出错,密钥就等于公开了。

3.2 开启目录接口的域级委托

这一步是整条链路的关键。在管理后台找到目录接口的授权设置,把上一步创建的服务账号的唯一标识(注意不是邮箱,是那串数字 ID)填进去,并勾选需要的权限范围。常见的范围有两类:只读的目录查询,和可写的用户管理。按最小必要原则,先只给只读,跑通查询后再按需加写权限。

填完之后建议等几分钟让配置生效。我遇到过刚配完立刻调用就报权限错、过五分钟再试就正常的情况,属于配置同步的延迟,不是代码问题。

3.3 环境与依赖:选一门顺手的语言

接口本身是标准的 HTTP + JSON,任何语言都能调。我日常用 Python,因为它的官方客户端库封装得比较完整,省去手写鉴权的麻烦。装依赖就一行:

pip install google-api-python-client google-auth google-auth-httplib2

如果你更习惯 Node 或 Java,官方也都有对应库,逻辑完全一致。选哪个不重要,重要的是鉴权部分尽量用官方库,别自己手搓签名,容易在时间戳、令牌刷新这些细节上翻车。

4. 鉴权代码怎么写:从密钥文件到可调用的客户端

4.1 构造凭证对象的核心三要素

用官方库鉴权,核心就是三个东西:密钥文件路径、要申请的权限范围、要代表哪个用户。前两个好理解,第三个是新手最容易懵的地方。

服务账号本身是个"空身份",它要访问的是某个具体网域的数据,所以需要指定一个"代表谁"——通常填一个有管理权限的账号,加上subject参数。这一步叫域内身份模拟,是让服务账号真正能操作目标域数据的关键。

from google.oauth2 import service_account from googleapiclient.discovery import build KEY_FILE = "/secure/path/service-account.json" SCOPES = ["https://www.googleapis.com/auth/admin.directory.user"] DELEGATED_USER = "admin@your-domain.example" creds = service_account.Credentials.from_service_account_file( KEY_FILE, scopes=SCOPES ).with_subject(DELEGATED_USER) service = build("admin", "directory_v1", credentials=creds)

4.2 权限范围怎么选:读和写要分开

权限范围(scope)决定了这个客户端能干什么。我强烈建议读和写用不同的凭证对象,或者至少在不同的脚本里用不同的范围。原因很简单:查询脚本天天跑,万一有 bug 也只是读到脏数据;写脚本一旦出错,可能批量改错几百个账号。把两者隔离,风险面立刻小一半。

常用的范围有这么几个:只读用户目录、读写用户目录、只读群组、读写群组。按需申请,别贪多。申请多了不仅不安全,某些范围还需要额外的管理员审批,反而拖慢进度。

4.3 第一次调用的验证方法

写完鉴权别急着上批量操作,先做一次最小验证:拉取当前域的前几个用户,打印出来看看。

results = service.users().list( domain="your-domain.example", maxResults=5, orderBy="email" ).execute() for u in results.get("users", []): print(u["primaryEmail"], u.get("orgUnitPath"))

能正常打印出邮箱和组织单位,说明鉴权链路、域级委托、权限范围三样全对了。这一步跑通,后面就是纯业务逻辑。

5. 批量管理多网域用户:查询、创建、停用的完整套路

5.1 分页拉取:别指望一次拿全

目录接口对单次返回数量有上限,默认一页最多几百条。想拿全量用户,必须处理分页令牌。很多人第一次写就踩坑:只调一次list,结果只拿到第一页,还以为组织里就这么多人。

正确做法是循环拉取,直到没有下一页令牌为止:

def list_all_users(service, domain): users, token = [], None while True: resp = service.users().list( domain=domain, maxResults=500, pageToken=token, orderBy="email" ).execute() users.extend(resp.get("users", [])) token = resp.get("nextPageToken") if not token: break return users

多网域场景下,把每个域当成一次独立调用,最后合并结果。注意不同域的domain参数要分别传,别想着一个参数通吃。

5.2 批量创建:先查重再写入

批量建号最怕的是重复创建。接口在遇到已存在的邮箱时通常会报错,但如果你不做前置判断,脚本跑到一半崩掉,前面建好的、后面没建的,状态就乱了。

我的做法是先拉一份现有邮箱集合,再拿待建清单去比对,只处理差集。写入时给每个账号设好初始密码、组织单位、必要的群组归属。密码建议用随机生成,并要求首次登录修改,避免弱口令。

existing = {u["primaryEmail"].lower() for u in list_all_users(service, domain)} for row in new_users: email = row["email"].lower() if email in existing: continue body = { "primaryEmail": email, "name": {"givenName": row["first"], "familyName": row["last"]}, "password": row["temp_password"], "changePasswordAtNextLogin": True, "orgUnitPath": row.get("ou", "/") } service.users().insert(body=body).execute()

5.3 停用而非删除:留好后悔药

离职处理我踩过一次大坑。早期图省事直接删账号,结果对方名下的云盘文件、邮件、日历全部连带清空,业务方找回来时已经晚了。后来统一改成先停用、后归档、确认无误再删。

停用只是把账号状态置为不可登录,数据全部保留,随时可以恢复。接口里就是改一个suspended字段:

service.users().update( userKey=email, body={"suspended": True} ).execute()

停用之后建议再补一步:把该账号的邮件和文件转移给其主管,这一步在后台有对应工具,接口侧也有相应能力,按组织规范走即可。

5.4 多网域批量操作的节奏控制

一次性对几百上千个账号做写操作,很容易触发接口的速率限制。我吃过这个亏:脚本跑得飞快,跑到一半开始大面积报错,重试又加重了限流。

解决办法有两个。一是加节流,每次写操作之间 sleep 一小段,比如 0.1 到 0.2 秒,牺牲一点速度换稳定。二是加退避重试,遇到限流错误时不要立刻重试,而是等指数增长的时间再试。这两招组合下来,批量任务基本能稳稳跑完。

6. 踩坑实录:那些报错信息背后的真实原因

6.1 "权限不足"但配置明明没错

这是最高频的坑。表现是接口返回 403,提示权限不足,但你反复检查服务账号、权限范围、域级委托,看起来都对。真实原因往往有三个:域级委托里填的是邮箱而不是数字 ID、配置刚改还没同步、模拟的用户本身没有对应管理权限。

排查顺序我总结成一条链:先确认委托里填的是服务账号的唯一数字 ID;再等五分钟重试排除同步延迟;最后确认subject那个账号确实有目录管理权限。三步走完,九成情况能定位。

6.2 查询返回空列表

接口不报错,但用户列表是空的。这种情况八成是domain参数写错了,或者你操作的域和授权委托的域不是同一个。多网域环境下特别容易混:服务账号在 A 域做了委托,你却拿它去查 B 域,自然查不到。

我的习惯是每个域配一套独立的凭证和配置,脚本里用配置文件区分,绝不硬编码。这样哪个域出问题,一眼就能看出来。

6.3 批量写入中途失败导致状态不一致

前面提过,批量写最怕中途崩。除了加节流和重试,我还加了一层断点续跑:每处理完一条就把结果写进本地日志,脚本重启时先读日志跳过已完成的。这样即使中途失败,重跑也不会重复操作。

日志格式很简单,一行一条:邮箱、操作类型、时间、结果。别小看这个,出问题时它就是你的"黑匣子"。

6.4 密钥泄露的应急处理

万一密钥文件不慎外泄,第一反应应该是立即在后台吊销这对密钥,然后重新生成一对、更新脚本配置。吊销是即时生效的,比改密码快得多。这也是服务账号相比普通账号的一个优势:凭证可以随时作废重发,不影响任何真人。

平时预防上,密钥文件绝不进代码仓库、绝不通过聊天工具传输、服务器上设最小读取权限。这三条守住,基本不会出大事。

7. 把脚本变成日常工具:调度、日志与权限复核

7.1 用定时任务跑周期性同步

查询类任务适合每天定时跑,比如每天凌晨拉一次全量用户清单,和内部系统比对,发现差异就告警。写类任务则建议手动触发或半自动,跑之前先出预览清单,人工确认后再执行。全自动写操作风险太高,除非你对自己的逻辑有绝对把握。

定时任务用系统自带的调度器就行,关键是把输出重定向到日志文件,方便事后查。别让脚本默默跑、默默失败,那样等于没跑。

7.2 日志要记到什么颗粒度

我的日志分两层。运行层记录每次任务的开始、结束、处理条数、失败条数;操作层记录每一条具体的增删改。运行层用来快速判断"今天有没有问题",操作层用来精确定位"哪一条出了问题"。

日志里不要记密码、密钥这类敏感信息,只记邮箱和操作类型即可。保留周期按组织合规要求来,一般至少留几个月。

7.3 定期复核服务账号的权限

服务账号的权限不是配一次就一劳永逸。业务变化了,可能原来需要的写权限现在不需要了,那就该收回。我建议每季度复核一次:这个账号还在用吗?权限范围还是最小必要吗?密钥是不是该轮换了?

密钥轮换是个好习惯,定期换一对新的、吊销旧的,能有效降低长期泄露风险。轮换时注意新旧密钥要有重叠期,避免脚本在切换瞬间失效。

8. 几个让效率翻倍的实操心得

第一,把常用操作封装成函数。查询、创建、停用、改组织单位,每个都写成独立函数,主流程只负责编排。这样复用性高,改起来也只改一处。

第二,配置文件与代码分离。域名、密钥路径、模拟用户这些全部放配置文件,代码里不出现任何硬编码。换环境时只改配置,不动代码。

第三,先小范围试跑。任何批量写操作,先拿三五个测试账号跑一遍,确认结果符合预期,再放开全量。这一步花几分钟,能省下几小时的补救。

第四,保留操作前的快照。批量改之前,先把涉及的账号当前状态导出成表格。万一改错了,照着快照能快速回滚。这个习惯救过我好几次。

第五,别追求一步到位。我最初想做一个"全自动账号生命周期系统",结果复杂度爆炸。后来拆成小步:先做查询、再做停用、最后做创建,每步跑稳了再往下走。慢即是快。

这套方案我从最初的几十个账号,一路用到管理多个网域、上千账号的规模,中间虽然踩了不少坑,但整体稳定性远超手动操作。核心就一句话:把重复的事交给接口,把判断的事留给人。服务账号加目录接口这套组合,恰好能帮你划清这条界线。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进