1. 批量管理用户的需求分析与整体设计思路
1.1 为什么企业环境离不开批量用户管理
不管你是刚接触Linux运维的新人,还是已经在生产环境摸爬滚打多年的老手,批量创建和删除用户这件事迟早都会找上你。我最早遇到这个需求是在一次服务器迁移项目中,原环境里有四十多个业务账号,分布在五六台服务器上,如果靠useradd一条一条敲,光录入信息就得半天,更别提还要设置密码、创建家目录、配置sudo权限——整个过程不仅慢,还特别容易出错,少敲一个参数或者多打一个字母,就可能造成账号无法登录或者权限错乱。
Shell脚本在这个场景下的价值,说白了就是把重复性的手工操作变成可重复执行的标准流程。你只需要维护一份用户清单,脚本会自动完成从用户创建、密码设置到目录初始化的全部工作。而且脚本方案几乎不依赖任何额外的软件包,只要是Linux系统就自带Bash环境,拿来就能用,这也是它相比Ansible、Puppet等配置管理工具最大的优势——轻量、直接、无侵入。
1.2 批量脚本的核心设计原则
我在写这类脚本之前,通常会先想清楚几个问题:用户数据从哪里来、密码怎么生成、创建过程出错怎么办、脚本能不能重复执行。这几点想明白了,脚本结构自然就清晰了。
第一,用户数据与逻辑分离。不要把所有用户名和密码硬编码在脚本里,而是单独放在一个文本文件里维护,比如users.txt,一行一个用户。这样以后新增员工或者有人离职,只需要改数据文件,脚本本身不用动。
第二,密码处理要兼顾安全和可追溯。批量创建用户时,密码有两种常见处理方式:一种是统一初始密码,用户首次登录后强制修改;另一种是每个用户随机生成独立密码,脚本把结果导出到文件交给管理员分发。我通常推荐后者,安全风险更低,也不容易出现一个账号被撞库导致全部账号沦陷的情况。
第三,脚本必须具备幂等性。也就是说,同一个脚本跑两次,结果是一致的,不会因为用户已经存在就报错中断,更不会把已有用户覆盖掉。这需要你在脚本里加入判断逻辑——用户存在就跳过或者提示,而不是盲目执行useradd。
第四,所有操作必须留痕。脚本运行过程中,哪些用户创建成功了、哪些失败了、失败原因是什么,都要写入日志文件。生产环境里出了问题,这份日志就是排查的第一手依据。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 批量创建用户的完整实现过程
2.1 用户数据清单的准备与格式设计
在动手写脚本之前,第一步是把用户清单准备好。我常用的格式有两种,一种是最简单的纯用户名列表:
code复制zhangsan
lisi
wangwu
zhaoliu
另一种是包含更多属性信息的格式,用冒号或者逗号分隔,比如用户名、UID、用户组、附加组、家目录路径:
code复制zhangsan:1003:devops:wheel:/home/zhangsan
lisi:1004:devops:docker:/home/lisi
wangwu:1005:ops:wheel:/data/wangwu
第二种格式适合需要精细控制用户属性的场景。比如张三和李四同属devops项目组,UID必须固定不能漂移(否则文件属主可能错乱),王五因为业务原因家目录要放在/data分区下,这些需求都能通过清单文件表达清楚。如果只是临时加一批测试账号,用第一种纯用户名列表就够了。
关于格式选择,我个人的经验是:优先用带字段分隔的清单格式,哪怕你暂时不需要那么多属性。因为脚本的生命周期往往比预期长,今天你只需要用户名,半年后可能就要加UID和组信息,到时候再改脚本结构反而麻烦。数据文件的字段设计可以前置一些,后面只需要扩展解析逻辑,整体框架不动。
2.2 密码生成策略与初次登录强制改密
用户密码是批量创建中最敏感的环节。如果你对安全性要求不高,比如只是内部测试环境,可以统一设一个初始密码,比如Init@123456,然后在脚本里带上chage -d 0 用户名这个命令,强制用户第一次登录时修改密码。这个命令的原理是把用户的密码最后修改日期归零,系统会在用户下次登录时强制要求设置新密码。
如果你对安全性要求高,或者用户数量不多,我更推荐用系统自带的openssl rand -base64 12随机生成密码。这个命令会输出一个12位的随机字符串,包含大小写字母、数字和特殊符号,强度足够应对大多数场景。脚本里可以这样写:
bash复制password=$(openssl rand -base64 12)
echo "用户名 初始密码" >> passwd_output.txt
echo "$password" | passwd --stdin "$username"
有些发行版不认passwd --stdin这个参数,比如Debian系默认就没有,这种情况下可以用chpasswd命令替代:
bash复制echo "$username:$password" | chpasswd
两种方式效果一样,就是把密码从标准输入读入并设置给指定用户。我建议写脚本时先判断一下系统类型再选择对应命令,或者直接用chpasswd,在CentOS、Ubuntu、Debian上实测都没问题。
2.3 创建脚本完整代码与逐段解析
下面给出一份我实际在用的批量创建用户脚本,代码不算复杂,但该有的判断和日志都覆盖了:
bash复制#!/bin/bash
# 批量创建用户脚本
# 用法: ./create_users.sh users.txt
USER_LIST="$1"
LOG_FILE="/var/log/user_create.log"
PASS_FILE="/root/initial_passwords.txt"
# 清空上次的密码记录文件
> "$PASS_FILE"
# 检查传入的用户清单文件是否存在
if [ ! -f "$USER_LIST" ]; then
echo "[ERROR] 用户清单文件不存在: $USER_LIST"
exit 1
fi
# 循环读取每一行,按冒号拆分字段
while IFS=: read -r username uid groups home_dir; do
# 跳过空行和以#开头的注释行
[ -z "$username" ] && continue
[[ "$username" == \#* ]] && continue
# 检查用户是否已存在
if id "$username" &>/dev/null; then
echo "[SKIP] 用户 $username 已存在,跳过创建"
echo "[SKIP] 用户 $username 已存在,跳过创建" >> "$LOG_FILE"
continue
fi
# 构造useradd参数
useradd_args="-m -s /bin/bash"
if [ -n "$uid" ]; then
useradd_args="$useradd_args -u $uid"
fi
if [ -n "$groups" ]; then
useradd_args="$useradd_args -G $groups"
fi
if [ -n "$home_dir" ]; then
useradd_args="$useradd_args -d $home_dir"
fi
# 执行创建
if [ -n "$home_dir" ]; then
useradd $useradd_args "$username"
else
useradd $useradd_args "$username"
fi
if [ $? -eq 0 ]; then
# 生成随机密码
password=$(openssl rand -base64 12)
echo "$username:$password" | chpasswd
# 强制首次登录修改密码
chage -d 0 "$username"
echo "$username $password" >> "$PASS_FILE"
echo "[OK] 用户 $username 创建成功"
echo "[OK] 用户 $username 创建成功" >> "$LOG_FILE"
else
echo "[ERROR] 用户 $username 创建失败,错误码 $?"
echo "[ERROR] 用户 $username 创建失败,错误码 $?" >> "$LOG_FILE"
fi
done < "$USER_LIST"
echo "全部处理完成,初始密码文件: $PASS_FILE"
这段代码的核心逻辑有这么几个关键点:
IFS=: read -r username uid groups home_dir这行会把每行文本按冒号切分成四个变量,IFS是Bash的内部字段分隔符,这里临时改成冒号,只对当前read命令生效。id "$username" &>/dev/null用来判断用户是否已经存在,&>/dev/null把标准输出和标准错误都丢弃,不让系统报错信息刷屏。useradd -m创建用户的同时生成家目录并把/etc/skel下的默认配置文件复制进去,这一步非常重要,没有-m的话用户登录后连.bash_profile都没有,环境变量加载会出问题。
2.4 批量创建中的细节优化与经验补充
脚本跑通了只是第一步,生产环境里你还需要处理很多边角问题。第一个是用户组是否提前创建。上面的脚本里,如果清单中指定的附加组不存在,useradd -G会报错并导致用户创建失败。解决方法是脚本开头先解析所有group字段,用groupadd把不存在的组先创建出来:
bash复制# 提取所有组名并去重
awk -F: '{print $3}' "$USER_LIST" | tr ',' '\n' | sort -u | while read grp; do
if [ -n "$grp" ] && ! grep -q "^$grp:" /etc/group; then
groupadd "$grp"
echo "[INFO] 创建用户组 $grp"
fi
done
第二个是sudo权限的分配。如果某些用户需要加入wheel组或sudo组来获得管理员权限,脚本里已经包含了通过-G参数指定附加组的逻辑,清单里写好就行。但要注意,组名在不同发行版上不一样,CentOS/RHEL系列是wheel,Ubuntu/Debian系列是sudo,跨平台使用时需要动态判断:
bash复制# 判断sudo组名
if grep -q "^sudo:" /etc/group; then
SUDO_GROUP="sudo"
else
SUDO_GROUP="wheel"
fi
第三个是失败重试机制。如果一个用户因为网络或者磁盘原因创建失败,脚本不会自动重试。我一般会在日志里标记失败记录,处理完一轮后再从日志中提取失败的用户重新执行一次,而不是整个清单从头跑一遍,这样效率更高。
3. 批量删除用户与清理残留数据
3.1 删除操作的安全策略与风险控制
批量删除用户比创建用户要更谨慎,因为删除是不可逆的操作——用户被删除后,他名下的文件可能变成无主状态,UID对应的文件权限会显示为一串数字,整个系统的文件权限体系都可能受到影响。我见过不少运维新手图省事,直接userdel -r 用户名一条命令全部删掉,结果把生产数据目录一起清空了,那个教训可太惨痛了。
所以在批量删除之前,我强烈建议先做三件事:
- 确认删除名单。把待删除的用户列表单独放到一个文件里,比如
delete_users.txt,不要和创建用户的清单混用。 - 备份用户数据。如果用户家目录里有业务数据,先打包备份到指定目录,万一误删还能恢复。
- 检查是否有正在运行的进程。如果用户有相关服务在跑,直接删除用户账号会导致进程权限错乱。先用
pgrep -u 用户名确认一下,有进程就先处理完再删。
3.2 删除脚本的两种模式:软删除与硬删除
我通常会根据场景把删除脚本分成两种模式:
软删除模式:不删家目录,只禁用账号。这种做法适合员工暂时离岗、账号需要冻结的场景,恢复起来很方便,只要把账号重新启用就行。
bash复制#!/bin/bash
# 批量禁用用户账号(软删除)
USER_LIST="$1"
LOG_FILE="/var/log/user_disable.log"
while read -r username; do
[ -z "$username" ] && continue
if id "$username" &>/dev/null; then
usermod -L "$username"
echo "$username 账号已锁定"
echo "$(date +%F_%T) $username 账号已锁定" >> "$LOG_FILE"
else
echo "$username 不存在,跳过"
fi
done < "$USER_LIST"
usermod -L这条命令本质是在/etc/shadow文件的密码字段前加上!,让该密码失效,达到禁止登录的目的。
硬删除模式:彻底删除用户及家目录。这是真正意义上的删除,脚本和命令如下:
bash复制#!/bin/bash
# 批量删除用户(硬删除,含家目录清理)
# 用法: ./delete_users.sh delete_users.txt
USER_LIST="$1"
LOG_FILE="/var/log/user_delete.log"
BACKUP_BASE="/backup/users"
while read -r username; do
[ -z "$username" ] && continue
if ! id "$username" &>/dev/null; then
echo "$username 不存在,跳过"
continue
fi
# 获取家目录路径
home_dir=$(getent passwd "$username" | cut -d: -f6)
# 备份家目录
if [ -n "$home_dir" ] && [ -d "$home_dir" ]; then
mkdir -p "$BACKUP_BASE"
tar czf "$BACKUP_BASE/${username}_$(date +%Y%m%d%H%M%S).tar.gz" "$home_dir"
echo "[BACKUP] 已备份 $home_dir 到 $BACKUP_BASE"
fi
# 检查用户进程
if pgrep -u "$username" &>/dev/null; then
echo "[WARN] 用户 $username 仍有进程在运行,强制删除前请确认"
echo "[WARN] 进程列表: $(pgrep -u $username | tr '\n' ' ')"
# 如果确认要删,取消下面一行的注释
# pkill -9 -u "$username"
fi
# 删除用户及家目录
userdel -r "$username"
if [ $? -eq 0 ]; then
echo "[OK] 用户 $username 已删除"
echo "$(date +%F_%T) [OK] 用户 $username 已删除" >> "$LOG_FILE"
else
echo "[ERROR] 用户 $username 删除失败"
echo "$(date +%F_%T) [ERROR] 用户 $username 删除失败" >> "$LOG_FILE"
fi
done < "$USER_LIST"
这段脚本有几个地方值得注意。getent passwd比直接读/etc/passwd更可靠,因为它会查询系统实际的数据源,包括NIS、LDAP等,不依赖本地文件。备份用tar czf打包而不是cp -r,是因为打包后能保留文件属主和权限信息,恢复时直接解压即可,而且备份文件体积更小,方便长期归档。
3.3 删除用户时的特有坑与避坑技巧
删除用户过程中的坑,最常见的三个:
第一个坑是userdel -r删除不干净。有些用户的家目录不在/etc/passwd里记录的路径下,或者用户拥有其他目录下的文件,这些文件不会随着userdel -r被删除。删除后需要全局扫描一下无主文件:
bash复制find / -nouser -o -nogroup 2>/dev/null
这条命令会找出系统中所有属主或属组不存在的文件,如果数量很多,说明有些文件的属主已经被删了,需要人工确认这些文件是保留还是清理。
第二个坑是UID冲突。如果删除了一个用户,但他的UID没有被复用,新用户创建时如果指定了相同UID,就会继承旧用户的所有文件访问权限。为了防止这种情况,删除用户后建议把UID保留一段时间再复用,或者在创建新用户时避免使用最近删除过的UID。
第三个坑是日志审计。删除用户这样敏感的操作,强烈建议保证脚本运行时的日志完整。我习惯在脚本开头记录执行时间、操作人、删除名单,在脚本结尾记录执行结果摘要。如果公司有日志审计系统,就把这些日志同步过去,出了问题能追责也能复盘。
4. 常见问题与排查技巧实录
4.1 批量操作失败的典型场景
我在实际使用这批脚本的过程中,踩过不少坑,这里整理几个最常见的问题和对应的排查思路,方便你遇到问题时快速定位:
| 问题现象 | 可能原因 | 排查方法 |
|---|---|---|
| 用户创建成功但无法登录 | 密码设置失败或未强制改密 | 执行passwd -S 用户名查看密码状态;手动执行chpasswd测试 |
useradd报错"group does not exist" |
清单中指定的附加组尚未创建 | 先执行组创建脚本,或检查/etc/group中组是否存在 |
| 用户家目录没有生成 | 缺少-m参数或/etc/skel配置异常 |
检查useradd命令是否包含-m;查看/etc/skel目录是否存在 |
| 随机密码含特殊字符导致登录异常 | 密码里的特殊字符被某些服务过滤 | 生成密码时去掉特殊字符,只保留字母和数字 |
| 批量删除后磁盘空间未释放 | userdel -r没有删除所有用户文件 |
执行find / -nouser扫描孤儿文件,手动确认删除 |
| 脚本执行时出现中文乱码 | 系统locale环境不是UTF-8 | 脚本开头export LANG=en_US.UTF-8或export LC_ALL=C |
4.2 遇到问题时的通用排查流程
如果你遇到的问题不在上表里,可以按下面的顺序排查:
第一步,看脚本输出的日志文件。我在脚本里都写了日志记录,这是第一手信息,能看出是哪个环节出的问题、错误码是什么。日志里标注了[OK]、[ERROR]、[SKIP],直接搜ERROR就能快速定位失败的用户。
第二步,手动执行一次单条命令。比如脚本里useradd报错,你就手动在命令行执行同样的useradd命令,看系统给出的完整错误信息。脚本里可能因为2>/dev/null把错误信息丢弃了,直接执行能看到真正的报错内容。
第三步,检查系统日志。创建用户相关的日志在/var/log/secure(CentOS)或/var/log/auth.log(Ubuntu)里,删除操作也会有记录。这些日志能告诉你系统层面的拒绝原因,比如SELinux拦截了操作、磁盘空间不足、/etc/passwd被锁等。
第四步,尝试在脚本关键位置加set -x。在脚本开头加上set -x,执行时系统会把每条命令和参数展开打印出来,能直观看到变量有没有正确传递、命令路径是否正确。排查完再删掉这一行,避免日志里全是调试信息。
4.3 自动化批量操作的几个进阶建议
脚本本身没问题之后,我还想分享几个让批量管理更省心的经验:
-
把命令封装成交互式菜单。如果你经常需要在创建和删除之间切换,可以写一个简单的菜单脚本,用
select命令提供选项,输入1执行创建、输入2执行删除,避免每次都要敲不同的脚本名和参数。 -
用循环搭配定时任务实现自动清理。如果有一批临时账号需要定期清理,比如外包人员的账号到期自动删除,可以结合
crontab在每周五晚上执行删除脚本。脚本里根据用户的创建日期判断是否超过有效期,超过的才删除,防止误删正常账号。 -
把脚本版本化放在Git仓库里。不要小看这一步,脚本是生产操作的直接载体,一旦出现过问题,回滚到上一个可用版本往往比现场改脚本快得多。而且Git记录能告诉你哪个版本改了什么东西,这对追溯问题非常有帮助。
-
测试环境一定要跑通再上生产。批量操作脚本哪怕逻辑再简单,也建议先在测试环境或者复制出来的容器环境里跑一遍完整流程,确认所有输出符合预期后再拿到生产环境执行。如果你手头没有测试环境,可以在虚拟机里跑,也可以把脚本里的
useradd改成echo模拟输出,检查逻辑是否正确再放开执行。
5. 总结与个人体会
写批量创建删除用户的脚本,本质上不是为了省那几次敲命令的时间,而是为了把用户管理这件事从"手工操作"变成"标准流程"。标准流程意味着可重复、可审计、可回滚,出了任何问题都能定位到具体环节。这是我做运维这些年最深刻的一个体会——凡是需要重复做的事,就应该脚本化;凡是脚本做的事,就必须留痕。
我在实际使用中发现,这类脚本的价值会随着时间积累越来越大。一开始你可能只是用来一次性创建几十个用户,跑完就放一边了。但后来你会发现,新员工入职要加账号、旧员工离职要删账号、项目组要临时开一批测试账号,这些都是同一个问题的不同场景。把脚本和数据文件准备好之后,整个流程只需要几分钟,而且不会因为操作者的水平差异而出现质量波动。
最后分享一个小技巧:我在脚本执行完成后,会习惯性地抽查一两个用户,用id 用户名和ls -ld /home/用户名确认创建结果,拿删除后的服务器来说,则会用getent passwd 用户名确认账号真的从系统数据库中消失了。这种"脚本跑完不等于事情做完"的习惯,帮我避免了很多潜在的隐患。希望这篇内容对你有帮助,也欢迎你在实际操作中根据自己的环境调整脚本细节。
