直接上脚本了
!/usr/bin/env bash
============================================================
MySQL / MariaDB / Percona 全版本 root 密码重置工具
用法:bash reset_all.sh --help
============================================================
set -uo pipefail
SCRIPT_NAME=(basename "0")SCRIPT_VERSION="2.0.0"
---------- 帮助 ----------
usage(){
cat <<EOF
{SCRIPT_NAME} {SCRIPT_VERSION} --- MySQL / MariaDB / Percona root 密码重置
用法:bash ${SCRIPT_NAME} 选项 <新密码> my.cnf路径
选项:-h, --help 显示本帮助并退出(不需要 root,不操作数据库)
-V, --version 显示脚本版本并退出
-c, --config <路径> 指定 my.cnf 路径;不指定则自动探测
--password <密码> 指定新密码(等价于位置参数,注意会出现在 ps 输出中)
--auth-plugin <插件> 仅 MySQL 5.7.6 ~ 8.3 生效,指定认证插件
例如 mysql_native_password、caching_sha2_password
--keep-network 临时阶段保留 TCP 监听(默认使用 skip-networking 关闭)
-- 结束选项解析,其后全部按位置参数处理
环境变量:NO_SKIP_NETWORKING=1 等价于 --keep-network
AUTH_PLUGIN=<插件名> 等价于 --auth-plugin
MYSQL_NEW_PASS=<密码> 通过环境变量传入新密码,避免出现在命令历史中
版本兼容:MySQL 4.1 ~ 5.6 SET PASSWORD ... = PASSWORD(),权限表字段 Password
MySQL 5.7.0 ~ 5.7.5 SET PASSWORD ... = PASSWORD(),权限表字段 Password
MySQL 5.7.6 ~ 5.7.x ALTER USER ... IDENTIFIED BY,字段 authentication_string
MySQL 8.0 ~ 8.3 ALTER USER ... IDENTIFIED BY,默认 caching_sha2_password
MySQL 8.4 ~ 9.x ALTER USER ... IDENTIFIED BY,mysql_native_password 已移除
MariaDB 5.5 ~ 10.1 SET PASSWORD ... = PASSWORD(),字段 Password
MariaDB 10.2 ~ 10.3 ALTER USER ... IDENTIFIED BY,字段 Password
MariaDB 10.4 ~ 11.x ALTER USER / SET PASSWORD,mysql.user 已为视图
执行前提:
- 必须以操作系统 root 执行(--help / --version 除外)
- 必须处于维护窗口,脚本会停止并重启数据库实例两次
- 建议先完成数据与配置备份
- 实例若处于复制链路,改密会写入 binlog 并向下游传播
工作原理:停止实例 → 以 skip-grant-tables 启动 → 在同一个连接内先 FLUSH PRIVILEGES
再执行改密语句 → 还原配置并正常重启 → 用新密码验证登录
注意:FLUSH PRIVILEGES 会重新加载 ACL 并恢复密码校验,因此改密语句必须与该命令在同一连接内完成;分连接执行会导致后续连接返回 ERROR 1045。
退出码:0 改密成功且新密码验证通过
1 执行失败(连接失败、改密失败、验证失败等)
2 参数用法错误
示例:查看帮助
bash ${SCRIPT_NAME} --help
最简用法(自动探测配置、socket、端口)bash ${SCRIPT_NAME} 'MyN3wP@ss'
指定配置文件bash ${SCRIPT_NAME} -c /etc/my.cnf 'MyN3wP@ss'
MySQL 8.0 需要兼容老客户端bash ${SCRIPT_NAME} --auth-plugin mysql_native_password 'MyN3wP@ss'
临时阶段保留网络监听bash ${SCRIPT_NAME} --keep-network 'MyN3wP@ss'
通过环境变量传密码(推荐,避免进入命令历史)MYSQL_NEW_PASS='MyN3wP@ss' bash $
安全提示:使用 --password 或位置参数时,密码会出现在 ps 与 shell 历史中;
生产环境建议改用 MYSQL_NEW_PASS 环境变量,并在执行后清理历史。
EOF
}
err(){ echo "错误 *" \>\&2; } info(){ echo "\[信息\] * "; }warn()
---------- 参数解析 ----------
NEW_PASS="${MYSQL_NEW_PASS:-}"
CONF=""
AUTH_PLUGIN="${AUTH_PLUGIN:-}"
KEEP_NET=0
while $# -gt 0 ; docase "$1" in
-h|--help) usage; exit 0 ;;
-V|--version) echo "{SCRIPT_NAME} {SCRIPT_VERSION}"; exit 0 ;;
-c|--config)
$# -ge 2 || { err "选项 $1 需要参数"; exit 2; }
CONF="$2"; shift 2 ;;
--config=) CONF="${1# =}"; shift ;;
--password)
$# -ge 2 || { err "选项 $1 需要参数"; exit 2; }
NEW_PASS="$2"; shift 2 ;;
--password=) NEW_PASS="${1# =}"; shift ;;
--auth-plugin)
$# -ge 2 || { err "选项 $1 需要参数"; exit 2; }
AUTH_PLUGIN="$2"; shift 2 ;;
--auth-plugin=) AUTH_PLUGIN="${1# =}"; shift ;;
--keep-network) KEEP_NET=1; shift ;;
--) shift; break ;;
-*) err "未知选项:1"; echo "使用 {SCRIPT_NAME} --help 查看帮助" >&2; exit 2 ;;
*)
if -z "$NEW_PASS" ; then NEW_PASS="$1"
elif -z "$CONF" ; then CONF="$1"
else err "多余的参数:1"; echo "使用 {SCRIPT_NAME} --help 查看帮助" >&2; exit 2
fi
shift ;;
esac
done
while $# -gt 0 ; doif -z "$NEW_PASS" ; then NEW_PASS="$1"
elif -z "$CONF" ; then CONF="$1"
else err "多余的参数:$1"; exit 2
fi
shift
done
if -z "$NEW_PASS" ; thenerr "缺少新密码参数"
echo "用法:bash ${SCRIPT_NAME} 选项 <新密码> my.cnf路径" >&2
echo "帮助:bash ${SCRIPT_NAME} --help" >&2
exit 2
fi
"$(id -u)" -ne 0 &&
case "$NEW_PASS" in"'" |"\" ) err "密码不能包含单引号或反斜杠"; exit 2 ;;
esac
---------- 1. 定位配置文件 ----------
if -z "$CONF" ; then
for f in /etc/my.cnf /etc/mysql/my.cnf /etc/mysql/mysql.conf.d/mysqld.cnf
/usr/local/mysql/etc/my.cnf /usr/local/mysql/my.cnf; do
-f "$f" && CONF="$f" && break
done
fi
if -z "$CONF" || ! -f "$CONF" ; then
err "未找到配置文件,请用 -c 指定"
exit 1
fi
info "配置文件:$CONF"
---------- 2. 定位 mysql 客户端 ----------
MYSQL_BIN=""
for p in "$(command -v mysql 2>/dev/null)"
/usr/local/mysql/bin/mysql /usr/bin/mysql /usr/local/bin/mysql; do
-n "$p" && -x "$p" && MYSQL_BIN="$p" && break
done
-z "$MYSQL_BIN" && { err "未找到 mysql 客户端"; exit 1; }
info "客户端:$MYSQL_BIN"
---------- 3. 推导 socket / port 候选 ----------
SOCK_CANDS=""; PORT_CANDS=""
MPD=$(command -v my_print_defaults 2>/dev/null || true)
if -n "$MPD" ; then
DEFAULTS=("MPD" --defaults-file="$CONF" mysqld client 2>/dev/null || true)
s=(printf '%s\\n' "DEFAULTS" | awk -F= '/^--socket=/{v=$2} END{print v}')
p=(printf '%s\\n' "DEFAULTS" | awk -F= '/^--port=/{v=$2} END{print v}')
-n "$s" && SOCK_CANDS="$s"
-n "$p" && PORT_CANDS="$p"
fi
for s in /tmp/mysql.sock /var/lib/mysql/mysql.sock /run/mysqld/mysqld.sock
/var/run/mysqld/mysqld.sock /usr/local/mysql/data/mysql.sock; do
SOCK_CANDS="SOCK_CANDS s"
done
s=$(ps -ef 2>/dev/null | grep -oE -- '--socket=\^ +' | head -1 | cut -d= -f2)
-n "$s" && SOCK_CANDS="s SOCK_CANDS"
for p in 3306 23306 3307 3308; do PORT_CANDS="PORT_CANDS p"; done
---------- 4. 识别服务管理方式 ----------
SVC_KIND=""; SVC_NAME=""
if command -v systemctl >/dev/null 2>&1; then
for s in mysqld mysql mariadb; do
if systemctl list-unit-files 2>/dev/null | grep -q "^${s}.service"; then
SVC_KIND="systemd"; SVC_NAME="$s"; break
fi
done
fi
if -z "$SVC_KIND" ; then
for s in mysql mysqld mariadb; do
if -x "/etc/init.d/$s" ; then
SVC_KIND="sysv"; SVC_NAME="$s"; break
fi
done
fi
-z "$SVC_KIND" && SVC_KIND="mysqld_safe"
info "服务管理:SVC_KIND {SVC_NAME:-无}"
---------- 5. 连接探测与服务控制 ----------
CONN_OPTS=""; CONN_DESC=""
sql()
probe(){local s p
for s in $SOCK_CANDS; do
-S "$s" || continue
if "MYSQL_BIN" --socket="s" -uroot --connect-timeout=5
-e "SELECT 1" >/dev/null 2>&1; then
CONN_OPTS="--socket=s"; CONN_DESC="socket=s"; return 0
fi
done
for p in $PORT_CANDS; do
if "MYSQL_BIN" -h127.0.0.1 -P"p" -uroot --connect-timeout=5
-e "SELECT 1" >/dev/null 2>&1; then
CONN_OPTS="-h127.0.0.1 -Pp"; CONN_DESC="tcp=127.0.0.1:p"; return 0
fi
done
return 1
}
wait_ready(){local i
for i in $(seq 1 60); do probe && return 0; sleep 2; done
return 1
}
start_normal(){case "$SVC_KIND" in
systemd) systemctl start "$SVC_NAME" ;;
sysv) "/etc/init.d/$SVC_NAME" start ;;
*)
local ms
ms=$(command -v mysqld_safe 2>/dev/null || echo /usr/local/mysql/bin/mysqld_safe)
nohup "$ms" >/tmp/mysql_reset_safe.log 2>&1 &
;;
esac
}
start_skip(){case "$SVC_KIND" in
systemd) systemctl start "$SVC_NAME" ;;
sysv) "/etc/init.d/$SVC_NAME" start ;;
*)
local ms
ms=$(command -v mysqld_safe 2>/dev/null || echo /usr/local/mysql/bin/mysqld_safe)
nohup "ms" --skip-grant-tables SKIP_NET >/tmp/mysql_reset_safe.log 2>&1 &
;;
esac
}
stop_db(){case "$SVC_KIND" in
systemd) systemctl stop "$SVC_NAME" ;;
sysv) "/etc/init.d/$SVC_NAME" stop ;;
*)
sql -e "SHUTDOWN;" >/dev/null 2>&1
sleep 3
if command -v pkill >/dev/null 2>&1; then
pkill -f mysqld_safe >/dev/null 2>&1
pkill -f 'mysqld ' >/dev/null 2>&1
fi
sleep 2
;;
esac
}
---------- 6. 备份与回滚 ----------
BK="{CONF}.bak.(date +%Y%m%d%H%M%S)"
cp -a "CONF" "BK" || { err "备份配置失败"; exit 1; }
info "配置已备份:$BK"
RESTORED=0restore_cfg(){
"$RESTORED" -eq 1 && return 0
RESTORED=1
info "停止实例并还原配置..."
stop_db
sleep 2
cp -a "BK" "CONF"
start_normal
info "配置已还原:$CONF"
}
trap 'restore_cfg; exit 1' INT TERM
---------- 7. 进入跳过权限校验模式 ----------
SKIP_NET=""
"$KEEP_NET" -ne 1 && "${NO_SKIP_NETWORKING:-0}" != "1" && SKIP_NET="--skip-networking"
info "停止数据库..."stop_db
sleep 3
if "$SVC_KIND" != "mysqld_safe" ; then{
echo ""
echo "mysqld"
echo "skip-grant-tables"
-n "$SKIP_NET" && echo "skip-networking"
} >> "$CONF"
fi
info "以跳过权限校验方式启动..."start_skip
if ! wait_ready; thenerr "120 秒内无法连接,请查看错误日志"
restore_cfg
exit 1
fi
info "已连接:$CONN_DESC"
---------- 8. 识别版本与分支 ----------
VER_RAW=$(sql -N -B -e "SELECT VERSION();" 2>/dev/null)
if -z "$VER_RAW" ; then
err "获取版本失败"
restore_cfg
exit 1
fi
info "版本:VER_RAW" case "VER_RAW" in
MariaDB ) FLAVOR="mariadb" ;;
*) FLAVOR="mysql" ;;
esac
NUM=(printf '%s\\n' "VER_RAW" | sed 's/^5.5.5-//'| grep -oE '0-9+.0-9+.0-9+' | head -1)
MAJOR={NUM%%.*}; R={NUM#* .}; MINOR={R%%.*}; PATCH={R#* .}
MAJOR={MAJOR:-0}; MINOR={MINOR:-0}; PATCH=${PATCH:-0}
info "识别:FLAVOR MAJOR.MINOR.PATCH"
ver_ge(){"$MAJOR" -gt "$1" && return 0
"$MAJOR" -lt "$1" && return 1
"$MINOR" -gt "$2" && return 0
"$MINOR" -lt "$2" && return 1
"$PATCH" -ge "$3" && return 0
return 1
}
---------- 9. 读取账号与运行态 ----------
USERS=$(sql -N -B -e "SELECT CONCAT(user,'@',host) FROM mysql.user WHERE user='root';" 2>/dev/null)
CREATE_ROOT=0
if -z "$USERS" ; then
warn "未查到 root 账号,将创建 root@localhost"
CREATE_ROOT=1
else
info "待重置账号:(printf '%s\\n' "USERS" | tr '\n' ' ')"
fi
RO_FIX=""ROV=$(sql -N -B -e "SELECT @@global.read_only;" 2>/dev/null)
"$ROV" = "1" && RO_FIX="$RO_FIX SET GLOBAL read_only=OFF;"
SROV=$(sql -N -B -e "SELECT @@global.super_read_only;" 2>/dev/null)
"$SROV" = "1" && RO_FIX="$RO_FIX SET GLOBAL super_read_only=OFF;"
PW_RELAX=""if "$FLAVOR" = "mysql" ; then
if ver_ge 8 0 0; then
COMP=$(sql -N -B -e "SELECT COUNT(*) FROM mysql.component WHERE component_urn LIKE '%validate_password%';" 2>/dev/null)
if "${COMP:-0}" -gt 0 2>/dev/null; then
PW_RELAX="SET GLOBAL validate_password.policy=LOW; SET GLOBAL validate_password.length=4;"
fi
elif ver_ge 5 7 0; then
VP=$(sql -N -B -e "SHOW VARIABLES LIKE 'validate_password_policy';" 2>/dev/null)
-n "$VP" && PW_RELAX="SET GLOBAL validate_password_policy=LOW; SET GLOBAL validate_password_length=4;"
fi
fi
---------- 10. 按版本生成改密语句 ----------
make_stmt(){
local u="{1%@*}" h="{1#* @}"
if "$FLAVOR" = "mariadb" ; then
if ver_ge 10 2 0; then
echo "ALTER USER 'u'@'h' IDENTIFIED BY '$NEW_PASS';"
else
echo "SET PASSWORD FOR 'u'@'h' = PASSWORD('$NEW_PASS');"
fi
else
if ver_ge 5 7 6; then
if -n "$AUTH_PLUGIN" ; then
echo "ALTER USER 'u'@'h' IDENTIFIED WITH AUTH_PLUGIN BY 'NEW_PASS';"
else
echo "ALTER USER 'u'@'h' IDENTIFIED BY '$NEW_PASS';"
fi
else
echo "SET PASSWORD FOR 'u'@'h' = PASSWORD('$NEW_PASS');"
fi
fi
}
---------- 11. 单连接执行(关键) ----------
OUT=$(
{
echo "FLUSH PRIVILEGES;"
-n "$RO_FIX" && echo "$RO_FIX"
-n "$PW_RELAX" && echo "$PW_RELAX"
if "$CREATE_ROOT" -eq 1 ; then
if "$FLAVOR" = "mysql" && ver_ge 8 0 0; then
echo "CREATE USER 'root'@'localhost' IDENTIFIED BY '$NEW_PASS';"
echo "GRANT ALL PRIVILEGES ON . TO 'root'@'localhost' WITH GRANT OPTION;"
else
echo "GRANT ALL PRIVILEGES ON . TO 'root'@'localhost' IDENTIFIED BY '$NEW_PASS' WITH GRANT OPTION;"
fi
else
for uh in USERS; do make_stmt "uh"; done
fi
echo "FLUSH PRIVILEGES;"
} | sql --force 2>&1
)
-n "$OUT" && printf '%s\n' "$OUT"
FAILED=0case "$OUT" in "ERROR") FAILED=1 ;; esac
---------- 12. 老版本兜底 ----------
if "$FAILED" -eq 1 && "$CREATE_ROOT" -eq 0 ; then
if "$FLAVOR" = "mysql" && ver_ge 5 7 6; then
warn "标准语句失败,请按上面的 ERROR 处理(多为密码策略校验,可改用更强密码)"
else
warn "标准语句失败,改为直接更新权限表..."
{
for uh in $USERS; do
u="{uh%@*}"; h="{uh#* @}"
if "$FLAVOR" = "mariadb" && ver_ge 10 4 0; then
echo "SET PASSWORD FOR 'u'@'h' = PASSWORD('$NEW_PASS');"
else
echo "UPDATE mysql.user SET Password=PASSWORD('NEW_PASS') WHERE user='u' AND host='$h';"
fi
done
echo "FLUSH PRIVILEGES;"
} | sql --force 2>&1
fi
fi
---------- 13. 还原并验证 ----------
info "还原配置并重启..."
restore_cfg
trap - INT TERM
info "等待实例就绪..."i=0
while "$i" -lt 60 ; do
MYSQL_PWD="NEW_PASS" "MYSQL_BIN" $CONN_OPTS -uroot
--connect-timeout=5 -e "SELECT 1" >/dev/null 2>&1 && break
i=$((i+1)); sleep 2
done
if MYSQL_PWD="NEW_PASS" "MYSQL_BIN" $CONN_OPTS -uroot--connect-timeout=5 -e "SELECT 1" >/dev/null 2>&1; then
info "完成:新密码验证通过"
exit 0
else
warn "新密码验证失败,请检查错误日志与上面的输出"
warn "配置备份:$BK"
exit 1
fi
使用示例
bash reset_all.sh --help
bash reset_all.sh -V
bash reset_all.sh -c /etc/my.cnf 'MyN3wP@ss'
MYSQL_NEW_PASS='MyN3wP@ss' bash reset_all.sh