Files
OJ2/docker/clear-sessions.sh
T
xuyueandClaude Opus 5.5 5813981bb1 fix(部署): Redis 强制口令,外接形态部署前实测
会话存在 Redis 里(session:<token> → 用户 id),能连上它就能伪造任意用户包括超管的
登录态。旧栈起的 oj-redis 发布在 0.0.0.0:5446、不带口令,2026-09-27 从公网实测
PING 得通。

- compose.debian.yml:REDIS_URL 用 ${REDIS_PASSWORD:?} 带上口令,缺了直接报错;
  自带数据形态的 oj-redis 加 --requirepass,端口只发布在 127.0.0.1
- deploy.sh:外接形态下实测「不带口令连不上、带 docker/.env 的口令连得上」,
  任一条不满足就中止部署
- clear-sessions.sh:从 docker/.env 读口令走 REDISCLI_AUTH;口令为空时不带 -e
  (空的 REDISCLI_AUTH 会发一条 AUTH "",实测会报一行 AUTH failed)
- compose.dev.yml:三个端口只绑 127.0.0.1
- docs/deploy.md 加「Redis 口令」一节:给旧栈 redis 加口令的一次性步骤

旧栈那份 compose 不在本仓库,线上 redis 的口令要照手册在服务器上手工加;
加之前 deploy.sh 会一直拦着。机房的 redis 不发布端口,不需要口令。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 14:51:52 +08:00

96 lines
4.3 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
#
# 一次性运维动作:清掉所有会话,强制全员重新登录。
#
# ## 为什么要跑
#
# 会话的反向索引 `user-sessions:<uid>` 是 498fc1c 才加的。在那之前签发的会话不在索引
# 里,revokeUserSessions 靠 SMEMBERS 找不到它们 —— 也就是说**改密码、重置密码、禁用
# 账号对这批会话统统无效**,只能等最长一个 SESSION_TTL_SECONDS(默认 7 天)自然过期。
#
# 学生密码是明文存着给老师查的,改密码正是密码泄露之后唯一的补救手段,这个空窗不能留。
# 代价只是所有人重新登录一次。
#
# 跑过一次就不用再跑了:此后签发的会话都带索引。
#
# ## 两个站点都要跑
#
# 机房和服务器**共用一个数据库,但各有各的 Redis**(见 compose.school.yml 头部)。
# 会话存在各自的 Redis 里,只清一边等于只解决一半。
#
# ## 为什么不是 FLUSHALL
#
# 同一个 Redis 里还装着 BullMQ 的判题队列(`bull:*`)。FLUSHALL 会把还在队列里的提交
# 一起丢掉,那些 submission 会永远停在 PENDING,只能超管逐条重判 —— 而重判还会把
# user_profile 的反范式计数带偏(见 apps/api/src/scripts/recount.ts)。
# 这里只删 `session:*` 和 `user-sessions:*`。
#
# ## 用法
#
# docker/clear-sessions.sh # 容器名默认 oj-redis
# CONTAINER=oj2-redis docker/clear-sessions.sh # 本机 dev
# YES=1 docker/clear-sessions.sh # 跳过确认
#
# `sh docker/clear-sessions.sh` 会用 dash 跑(Debian 的 /bin/sh 就是 dash),而下面
# 那行的 pipefail 是 bash 专有的,一上来就报 `Illegal option -o pipefail`。
# 这行必须在 set 之前,且只能用 dash 也认的语法。deploy.sh 里是同一道垫片。
[ -n "${BASH_VERSION:-}" ] || exec bash "$0" "$@"
set -euo pipefail
CONTAINER="${CONTAINER:-oj-redis}"
# redis 带口令时(服务器那台),redis-cli 靠 REDISCLI_AUTH 认证。没显式给就从
# docker/.env 里读;机房和本机 dev 的 redis 不带口令,读不到就留空,不影响。
if [ -z "${REDIS_PASSWORD:-}" ] && [ -f "$(dirname "$0")/.env" ]; then
REDIS_PASSWORD=$(grep -E '^REDIS_PASSWORD=' "$(dirname "$0")/.env" | tail -1 | cut -d= -f2- || true)
fi
# 口令为空时连 -e 都不能带:空的 REDISCLI_AUTH 会让 redis-cli 发一条 AUTH "",
# 在不带口令的 redis 上报一行 AUTH failed(实测,虽然随后命令照常执行)。
EXEC=(docker exec)
[ -z "${REDIS_PASSWORD:-}" ] || EXEC+=(-e REDISCLI_AUTH="$REDIS_PASSWORD")
EXEC+=("$CONTAINER" redis-cli)
cli() { "${EXEC[@]}" "$@"; }
say() { printf '\n\033[1;36m==> %s\033[0m\n' "$*"; }
ok() { printf ' \033[32m✓\033[0m %s\n' "$*"; }
die() { printf '\n\033[1;31m❌ %s\033[0m\n\n' "$*" >&2; exit 1; }
[ "$(cli ping 2>/dev/null)" = PONG ] \
|| die "连不上容器 $CONTAINER 里的 redis(用 CONTAINER=... 指定容器名;带口令的话检查 REDIS_PASSWORD)"
# 用 SCAN 而不是 KEYS:KEYS 会阻塞住整个 Redis,而这上面还挂着判题队列和所有人的
# 会话读写。xargs 分批是因为单条 DEL 传太多 key 会顶到命令行长度上限;每批 DEL 返回
# 删掉的个数,累加起来就是总数。
purge() {
cli --scan --pattern "$1" 2>/dev/null \
| xargs -r -n 400 "${EXEC[@]}" del \
| awk '{ sum += $1 } END { print sum + 0 }'
}
count() { cli --scan --pattern "$1" 2>/dev/null | wc -l; }
say "容器 $CONTAINER"
before_sessions=$(count 'session:*')
before_index=$(count 'user-sessions:*')
before_bull=$(count 'bull:*')
printf ' session:* %s\n' "$before_sessions"
printf ' user-sessions:* %s\n' "$before_index"
printf ' bull:* %s(不动)\n' "$before_bull"
if [ "${YES:-}" != "1" ]; then
read -rp $'\n 删掉上面的会话、让所有人重新登录?[y/N] ' answer
[ "$answer" = "y" ] || [ "$answer" = "Y" ] || die "已取消,什么都没做"
fi
say "清理"
ok "session:* 删掉 $(purge 'session:*') 个"
ok "user-sessions:* 删掉 $(purge 'user-sessions:*') 个"
after_bull=$(count 'bull:*')
[ "$after_bull" = "$before_bull" ] \
|| die "判题队列的键数变了($before_bull → $after_bull),这不该发生,请人工检查"
ok "bull:* 仍是 $after_bull 个,判题队列没被动过"
say "完成 —— 另一个站点的 Redis 也要跑一遍"