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>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-09-27 14:51:52 +08:00
1 parent 9d271942aa
commit 5813981bb1
6 files changed
+73 -10

No files matched your search

+8
View File
@@ -12,6 +12,14 @@ POSTGRES_PASSWORD=
# openssl rand -hex 32
OJ2_JUDGE_TOKEN=
# Redis 口令。会话存在 Redis 里,能连上它就能伪造任意用户(包括超管)的登录态,
# 所以必须设。它会拼进 REDIS_URL,只能用 URL 安全的字符,自己生成:
# openssl rand -hex 32
# 自带数据形态下本栈的 oj-redis 直接用它;外接旧栈 redis 时,那边的 redis-server
# 也要带上同一个 --requirepass(见 docs/deploy.md「Redis 口令」)。
# 机房的 redis 不发布端口,只在 compose 网络里,不需要设。
REDIS_PASSWORD=
# 判题并发。受判题沙箱能力限制,不是越大越好。
# 服务器 2,机房机器宽裕可以给 4。
JUDGE_CONCURRENCY=2
+17 -5
View File
@@ -40,23 +40,35 @@ 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; }
docker exec "$CONTAINER" redis-cli ping >/dev/null 2>&1 \
|| die "连不上容器 $CONTAINER 里的 redis(用 CONTAINER=... 指定容器名)"
[ "$(cli ping 2>/dev/null)" = PONG ] \
|| die "连不上容器 $CONTAINER 里的 redis(用 CONTAINER=... 指定容器名;带口令的话检查 REDIS_PASSWORD)"
# 用 SCAN 而不是 KEYS:KEYS 会阻塞住整个 Redis,而这上面还挂着判题队列和所有人的
# 会话读写。xargs 分批是因为单条 DEL 传太多 key 会顶到命令行长度上限;每批 DEL 返回
# 删掉的个数,累加起来就是总数。
purge() {
docker exec "$CONTAINER" redis-cli --scan --pattern "$1" 2>/dev/null \
| xargs -r -n 400 docker exec "$CONTAINER" redis-cli del \
cli --scan --pattern "$1" 2>/dev/null \
| xargs -r -n 400 "${EXEC[@]}" del \
| awk '{ sum += $1 } END { print sum + 0 }'
}
count() { docker exec "$CONTAINER" redis-cli --scan --pattern "$1" 2>/dev/null | wc -l; }
count() { cli --scan --pattern "$1" 2>/dev/null | wc -l; }
say "容器 $CONTAINER"
before_sessions=$(count 'session:*')
+9 -2
View File
@@ -61,10 +61,16 @@ services:
# redis 里没有非丢不可的东西(会话、判题队列),用旧的那个也无所谓 ——
# 旧后端已经停了,新后端独占它,剩下的 Django / dramatiq 残留键前缀不同,互不干扰。
profiles: ["local-data"]
# 会话存在这里:谁能连上它,谁就能写一条 session:<token> 冒充超管。所以必须带密码,
# 端口也只发布在本机(api / worker 走 compose 网络连 oj-redis,用不着宿主机端口)。
command: ["redis-server", "--requirepass", "${REDIS_PASSWORD:?}"]
environment:
# redis-cli 认这个变量,healthcheck 和 docker exec 进来的 redis-cli 都不用再带 -a
REDISCLI_AUTH: ${REDIS_PASSWORD:?}
volumes:
- ${DATA_DIR:-../data}/redis:/data
ports:
- "5446:6379"
- "127.0.0.1:5446:6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
@@ -134,7 +140,8 @@ services:
- ${DATA_DIR:-../data}/backend:/data
environment: &api-env
DATABASE_URL: postgres://onlinejudge:${POSTGRES_PASSWORD:?}@${DB_HOST:-oj-postgres}:${DB_PORT:-5432}/onlinejudge
REDIS_URL: redis://${REDIS_HOST:-oj-redis}:${REDIS_PORT:-6379}
# 密码拼进 URL,所以只能用 URL 安全的字符 —— 用 openssl rand -hex 32 生成
REDIS_URL: redis://:${REDIS_PASSWORD:?请在 docker/.env 里设置 REDIS_PASSWORD}@${REDIS_HOST:-oj-redis}:${REDIS_PORT:-6379}
JUDGE_SERVER_URL: http://oj-judge:8080
JUDGE_SERVER_TOKEN: ${OJ2_JUDGE_TOKEN:?}
JUDGE_CONCURRENCY: ${JUDGE_CONCURRENCY:-2}
+3 -3
View File
@@ -35,7 +35,7 @@ services:
POSTGRES_USER: onlinejudge
POSTGRES_PASSWORD: onlinejudge
ports:
- "5433:5432" # 5433 避开可能存在的本机 5432
- "127.0.0.1:5433:5432" # 5433 避开可能存在的本机 5432
volumes:
- oj2-pgdata:/var/lib/postgresql/data
healthcheck:
@@ -49,7 +49,7 @@ services:
container_name: oj2-redis
restart: unless-stopped
ports:
- "6380:6379" # 6380 避开可能存在的本机 6379
- "127.0.0.1:6380:6379" # 6380 避开可能存在的本机 6379
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
@@ -75,7 +75,7 @@ services:
tmpfs:
- /tmp
ports:
- "8081:8080"
- "127.0.0.1:8081:8080"
volumes:
- ../data/test_case:/test_case:ro
- ../data/judge_server/log:/log
+11
View File
@@ -175,6 +175,17 @@ if [ "$LOCAL_DATA" -eq 0 ]; then
|| die "外接形态依赖的 $c 没在跑 —— 先 docker compose -f /root/OJDeploy/docker-compose.yml up -d $c"
done
ok "外接的 postgres / redis 都在跑"
# 外接的 redis 不归本栈管,口令是否真的生效只能在这里查。旧栈那份 compose 的 redis
# 原本没有口令、端口发布在公网上 —— 会话存在里面,等于人人可以伪造超管登录态。
# 不带口令能 PING 通就说明那边还没加 --requirepass,这种状态不许上线。
redis_pw=$(grep -E '^REDIS_PASSWORD=' docker/.env | tail -1 | cut -d= -f2- || true)
[ -n "$redis_pw" ] || die "docker/.env 里没有 REDIS_PASSWORD(openssl rand -hex 32 生成)"
[ "$(docker exec -e REDISCLI_AUTH= oj-redis redis-cli ping 2>/dev/null)" != PONG ] \
|| die "外接的 oj-redis 不带口令也能连上 —— 先给它加 --requirepass,见 docs/deploy.md「Redis 口令」"
[ "$(docker exec -e REDISCLI_AUTH="$redis_pw" oj-redis redis-cli ping 2>/dev/null)" = PONG ] \
|| die "用 docker/.env 的 REDIS_PASSWORD 连不上外接的 oj-redis —— 两边口令不一致"
ok "外接的 redis 要求口令,且和 docker/.env 一致"
else
ok "postgres / redis 由本栈起,不预检"
fi