diff --git a/docker/.env.example b/docker/.env.example index 578347e..7a29158 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -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 diff --git a/docker/clear-sessions.sh b/docker/clear-sessions.sh index 42b34c2..b905824 100755 --- a/docker/clear-sessions.sh +++ b/docker/clear-sessions.sh @@ -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:*') diff --git a/docker/compose.debian.yml b/docker/compose.debian.yml index 4362e7d..cdafc09 100644 --- a/docker/compose.debian.yml +++ b/docker/compose.debian.yml @@ -61,10 +61,16 @@ services: # redis 里没有非丢不可的东西(会话、判题队列),用旧的那个也无所谓 —— # 旧后端已经停了,新后端独占它,剩下的 Django / dramatiq 残留键前缀不同,互不干扰。 profiles: ["local-data"] + # 会话存在这里:谁能连上它,谁就能写一条 session: 冒充超管。所以必须带密码, + # 端口也只发布在本机(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} diff --git a/docker/compose.dev.yml b/docker/compose.dev.yml index b144420..0f44b98 100644 --- a/docker/compose.dev.yml +++ b/docker/compose.dev.yml @@ -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 diff --git a/docker/deploy.sh b/docker/deploy.sh index 61ceb6e..c50d377 100755 --- a/docker/deploy.sh +++ b/docker/deploy.sh @@ -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 diff --git a/docs/deploy.md b/docs/deploy.md index b59012f..fa571d6 100644 --- a/docs/deploy.md +++ b/docs/deploy.md @@ -33,6 +33,31 @@ Redis)和 WebSocket 推送(本地 pub/sub)是每站独立的 —— 学生 沿用旧数据却忘了设它,会静默挂上一堆空目录:空库、没测试点、题面图片 404, **而且不报错**。这是整个部署里唯一会静默走歪的地方,`deploy.sh` 专门为它设了一道自检。 +## Redis 口令 + +**会话存在 Redis 里**(`session:` → 用户 id),谁能连上它,谁就能写一条会话 +冒充任意用户,包括超管。所以服务器那台的 Redis **必须带口令**:`compose.debian.yml` +的 `REDIS_URL` 用 `${REDIS_PASSWORD:?}` 强制要求,`deploy.sh` 在外接形态下还会实测 +「不带口令连不上、带 `docker/.env` 的口令连得上」,两条有一条不满足就中止部署。 + +2026-09-27 以前,旧栈 compose 起的那个 `oj-redis` 发布在 `0.0.0.0:5446`、不带口令, +**从公网直接 PING 得通**(实测过)。给它加口令的一次性步骤,在服务器上: + +```bash +cd /root/OJDeploy +PW=$(openssl rand -hex 32) +echo "REDIS_PASSWORD=$PW" >> OJ2/docker/.env +# 编辑 docker-compose.yml 的 oj-redis,加一行($PW 换成上面的值,别提交到任何仓库): +# command: ["redis-server", "--requirepass", ""] +docker compose up -d oj-redis # 重建 redis,此刻起旧口令(无)的连接全部失败 +cd OJ2 && docker/deploy.sh # 马上重新部署,api / worker 带上口令 +``` + +两步之间 api 连不上 Redis,站点会 500 一两分钟,挑没人用的时候做。 +会话会保留(redis 退出时落盘 RDB),学生不用重新登录。 + +机房那台的 `oj-redis` 不发布端口、只在 compose 网络里,不需要口令。 + ## 上线 ### 服务器:push 就部署