分享一个MySQL/MariaDB/Percona全版本root密码重置工具
直接上脚本了
!/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 ]; do
case "$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 ]; do
if [ -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" ]; then
err "缺少新密码参数"
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 -P$p"; 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 '[m]ysqld ' >/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=0
restore_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; then
err "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=0
case "$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
浙公网安备 33010602011771号