init: CasSsoService CAS 5.3 overlay (cas.war via LFS)
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
# 本地推送 / 远端部署 用的环境变量示例
|
||||
# 用法:复制为 .env 或直接 export 后执行脚本
|
||||
|
||||
# ---- ACR 阿里云镜像仓库 ----
|
||||
ACR_NAMESPACE=your-namespace # 必填:阿里云镜像命名空间
|
||||
ACR_USERNAME=your-acr-username # 必填:阿里云仓库登录用户名
|
||||
ACR_PASSWORD=your-acr-password # 必填:阿里云仓库独立登录密码
|
||||
ACR_REGISTRY_PUBLIC=registry.cn-hangzhou.aliyuncs.com
|
||||
|
||||
# ---- 部署到远端 ----
|
||||
DEPLOY_HOST=192.168.10.157 # ECS 可达地址
|
||||
DEPLOY_PORT=22
|
||||
DEPLOY_USER=yunhai
|
||||
REMOTE_DIR=/opt/docker/apps/cas-sso
|
||||
SSHPASS=123456 # 远端登录密码(remote_run/upload 用)
|
||||
|
||||
# ---- CAS 自带 MySQL 密码 ----
|
||||
MYSQL_PASSWORD=Cas@123456
|
||||
MYSQL_ROOT_PASSWORD=Root@123456
|
||||
@@ -0,0 +1,23 @@
|
||||
# CAS 5.3.16 镜像(基于已构建好的 target/cas.war)
|
||||
# 构建上下文 = 项目根目录: docker build -f docker/Dockerfile -t <img> .
|
||||
FROM eclipse-temurin:8-jre
|
||||
|
||||
# CAS 5.3 默认要求 HTTPS(安全 Cookie),这里生成自签名密钥库(密码固定 changeit,过渡期用)
|
||||
RUN mkdir -p /etc/cas/config \
|
||||
&& keytool -genkeypair -alias cas -keyalg RSA -keysize 2048 -validity 3650 \
|
||||
-storetype JKS -keystore /etc/cas/cas.keystore \
|
||||
-storepass changeit -keypass changeit -dname "CN=cas.example.org" 2>/dev/null || true \
|
||||
&& apt-get update && apt-get install -y --no-install-recommends curl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
COPY docker/cas.war /app/cas.war
|
||||
COPY docker/cas.properties /etc/cas/config/cas.properties
|
||||
COPY docker/run-cas.sh /app/run-cas.sh
|
||||
COPY etc/cas/config/log4j2.xml /etc/cas/config/log4j2.xml
|
||||
RUN chmod +x /app/run-cas.sh
|
||||
|
||||
# 镜像内 cas.properties 仅作「nacos 不可达时的兜底默认」;
|
||||
# 正常启动由 run-cas.sh 从 nacos 拉取线上配置(dataId=cas-prod.properties)后覆盖。
|
||||
EXPOSE 8443
|
||||
ENTRYPOINT ["/app/run-cas.sh"]
|
||||
@@ -0,0 +1,31 @@
|
||||
# ============================================================
|
||||
# 公共基础配置 base-common
|
||||
# 部署位置:Nacos namespace=public group=DEFAULT_GROUP dataId=base-common.properties
|
||||
# 用途:所有服务共享的 mysql / redis / rabbitmq 连接信息(只放连接,不放业务库名)
|
||||
# CAS 的 run-cas.sh 会读取 spring.datasource.* 拼出 JDBC URL,
|
||||
# 库名在 cas-prod.properties 的 cas.authn.jdbc.query[0].database 中指定(如 bsch_userbase)
|
||||
# ============================================================
|
||||
|
||||
# ---------- MySQL(公共 mysql8,服务名 mysql8,位于 infra-infra-net)----------
|
||||
spring.datasource.host=mysql8
|
||||
spring.datasource.port=3306
|
||||
spring.datasource.username=__SHARED_MYSQL_USER__
|
||||
spring.datasource.password=__SHARED_MYSQL_PASSWORD__
|
||||
spring.datasource.params=useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
|
||||
|
||||
# ---------- Redis ----------
|
||||
spring.redis.host=__REDIS_HOST__
|
||||
spring.redis.port=6379
|
||||
spring.redis.password=__SHARED_REDIS_PASSWORD__
|
||||
spring.redis.database=0
|
||||
spring.redis.timeout=5000
|
||||
spring.redis.jedis.pool.max-active=8
|
||||
spring.redis.jedis.pool.max-idle=8
|
||||
spring.redis.jedis.pool.min-idle=0
|
||||
|
||||
# ---------- RabbitMQ ----------
|
||||
spring.rabbitmq.host=__RABBITMQ_HOST__
|
||||
spring.rabbitmq.port=5672
|
||||
spring.rabbitmq.username=__SHARED_RABBIT_USER__
|
||||
spring.rabbitmq.password=__SHARED_RABBIT_PASSWORD__
|
||||
spring.rabbitmq.virtual-host=/
|
||||
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env bash
|
||||
# 本地构建 CAS 镜像并推送到阿里云「公网」 registry
|
||||
# 用法: ./build-push.sh <version>
|
||||
# 前置环境变量: ACR_NAMESPACE, ACR_USERNAME, ACR_PASSWORD
|
||||
set -euo pipefail
|
||||
VERSION="${1:-latest}"
|
||||
REGISTRY="${ACR_REGISTRY_PUBLIC:-registry.cn-hangzhou.aliyuncs.com}"
|
||||
NAMESPACE="${ACR_NAMESPACE:?请设置 ACR_NAMESPACE(阿里云镜像命名空间)}"
|
||||
IMG="$REGISTRY/$NAMESPACE/yunhai:$VERSION"
|
||||
|
||||
cd "$(dirname "$0")/.." # 切到项目根(构建上下文需含 target/cas.war)
|
||||
echo "== build $IMG =="
|
||||
docker build -f docker/Dockerfile -t "$IMG" .
|
||||
echo "== login $REGISTRY =="
|
||||
echo "$ACR_PASSWORD" | docker login --username="$ACR_USERNAME" --password-stdin "$REGISTRY"
|
||||
echo "== push =="
|
||||
docker push "$IMG"
|
||||
echo "PUSHED $IMG"
|
||||
@@ -0,0 +1,29 @@
|
||||
# CAS 自身配置(部署到 Nacos:namespace=cas, group=CAS, dataId=cas-prod.properties)
|
||||
# 说明:
|
||||
# - JDBC 的连接信息(URL / 账号 / 密码)由公共 base-common 注入,这里【不要】写 url/user/password
|
||||
# - 这里只写「库名」(database) 与查询/密码编码相关项
|
||||
# - 其余 CAS 专属配置(tgc / webflow / server / custom.rsa 等)保持你现有的不变
|
||||
|
||||
cas.server.name: https://localhost:8443
|
||||
cas.server.prefix: https://localhost:8443/cas
|
||||
|
||||
cas.serviceRegistry.initFromJson: true
|
||||
cas.logout.followServiceRedirects: true
|
||||
|
||||
# ===== JDBC 认证(库名写这里,连接来自 base-common)=====
|
||||
cas.authn.jdbc.query[0].database=bsch_userbase
|
||||
cas.authn.jdbc.query[0].sql=select * from sys_user where username=?
|
||||
cas.authn.jdbc.query[0].fieldPassword=password
|
||||
cas.authn.jdbc.query[0].fieldDisabled=disabled
|
||||
cas.authn.jdbc.query[0].fieldExpired=expired
|
||||
cas.authn.jdbc.query[0].passwordEncoder.type=BCRYPT
|
||||
cas.authn.jdbc.query[0].passwordEncoder.strength=10
|
||||
|
||||
# 自定义 /api/login 登录成功后的默认跳转地址(URL 不带 service 参数时生效)。
|
||||
# - 支持绝对地址,如 http://sso.yunhaixingtong.com(绝对地址会直接作为跳转目标,不再拼 CAS 自身 host)
|
||||
# - 也支持相对路径,如 / 或 /cas/login
|
||||
# - 不配此键时,代码内置默认值 = http://sso.yunhaixingtong.com
|
||||
# 带 service 参数时,仍按原逻辑跳转该 service(控制器返回带 ticket 的 service 地址)。
|
||||
custom.login.default-redirect-url=http://sso.yunhaixingtong.com
|
||||
|
||||
# ... 其余 CAS 配置(tgc / webflow / custom.rsa / custom.login 等)同你现有内容 ...
|
||||
@@ -0,0 +1,28 @@
|
||||
# CAS 烤进镜像的兜底配置(仅当 Nacos 不可用时由 run-cas.sh 回退使用)
|
||||
# 正常情况下以 Nacos 的 cas-prod.properties(连接信息来自 base-common)为准
|
||||
cas.server.name: https://localhost:8443
|
||||
cas.server.prefix: https://localhost:8443/cas
|
||||
|
||||
cas.adminPagesSecurity.ip=127\.0\.0\.1
|
||||
|
||||
logging.config: file:/etc/cas/config/log4j2.xml
|
||||
|
||||
server.port: 8443
|
||||
server.ssl.enabled: true
|
||||
server.ssl.key-store: /etc/cas/cas.keystore
|
||||
server.ssl.key-store-password: changeit
|
||||
server.ssl.key-password: changeit
|
||||
server.ssl.key-alias: cas
|
||||
|
||||
# ===== 数据库认证(兜底:连公共 mysql8 上的 bsch_userbase)=====
|
||||
# 注意:以下 user/password 为占位符;Nacos 不可用时需改为公共 mysql8 真实账号
|
||||
cas.authn.jdbc.query[0].sql: select * from sys_user where username=?
|
||||
cas.authn.jdbc.query[0].url: jdbc:mysql://mysql8:3306/bsch_userbase?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
|
||||
cas.authn.jdbc.query[0].user: __SHARED_MYSQL_USER__
|
||||
cas.authn.jdbc.query[0].password: __SHARED_MYSQL_PASSWORD__
|
||||
cas.authn.jdbc.query[0].driverClass: com.mysql.jdbc.Driver
|
||||
cas.authn.jdbc.query[0].fieldPassword: password
|
||||
cas.authn.jdbc.query[0].fieldDisabled: disabled
|
||||
cas.authn.jdbc.query[0].fieldExpired: expired
|
||||
cas.authn.jdbc.query[0].passwordEncoder.type: BCRYPT
|
||||
cas.authn.jdbc.query[0].passwordEncoder.strength: 10
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d777752b9fa45e17dd5e96aafef6ca9ae16f5fb30a9d042424c2a33b739747b6
|
||||
size 173749577
|
||||
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env bash
|
||||
# 一键部署:上传编排文件 -> SSH 到 ECS 拉取镜像并启动(CAS,依赖公共 mysql8/redis/rabbitmq)
|
||||
# 用法: ./deploy.sh <version>
|
||||
# 前置环境变量:
|
||||
# DEPLOY_HOST (必填) ECS 公网/私网 IP 或域名(SSH 目标,不是 ACR 仓库地址!)
|
||||
# SSHPASS (必填) ECS 上 $DEPLOY_USER 的 SSH 密码
|
||||
# ACR_NAMESPACE (必填) 阿里云镜像命名空间,如 damaomi_docker
|
||||
# ACR_PASSWORD (可选) 若提供则在 ECS 上自动 docker login,避免 ECS 未登录导致 pull 失败
|
||||
# ACR_USERNAME (可选) 默认 15121037482
|
||||
# DEPLOY_USER / DEPLOY_PORT / REMOTE_DIR 均有默认值
|
||||
set -euo pipefail
|
||||
VERSION="${1:-latest}"
|
||||
HOST="${DEPLOY_HOST:?请设置 DEPLOY_HOST(ECS 的 IP/域名,不是 ACR 仓库地址)}"
|
||||
PORT="${DEPLOY_PORT:-22}"
|
||||
USER="${DEPLOY_USER:-yunhai}"
|
||||
REMOTE_DIR="${REMOTE_DIR:-/opt/docker/apps/cas-sso}"
|
||||
ACR_NAMESPACE="${ACR_NAMESPACE:?请设置 ACR_NAMESPACE(如 damaomi_docker)}"
|
||||
ACR_USERNAME="${ACR_USERNAME:-15121037482}"
|
||||
ACR_PASSWORD="${ACR_PASSWORD:-}"
|
||||
# 镜像走公网端点(内网 registry-vpc 在不在 VPC 的 ECS 上连不通)
|
||||
ACR_REGISTRY="${ACR_REGISTRY:-registry.cn-hangzhou.aliyuncs.com}"
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
echo "== 1/2 上传 compose + run-cas.sh + init.sql + cas.war 到 $HOST:$REMOTE_DIR =="
|
||||
python3 upload.py "$HOST" "$PORT" "$USER" \
|
||||
docker-compose.cas.yml "$REMOTE_DIR/docker-compose.cas.yml" \
|
||||
run-cas.sh "$REMOTE_DIR/run-cas.sh" \
|
||||
init.sql "$REMOTE_DIR/init.sql"
|
||||
|
||||
# 若提供了 ACR 密码,生成自动登录命令(避免 ECS 未登录导致 pull 失败)
|
||||
LOGIN_CMD=""
|
||||
if [ -n "$ACR_PASSWORD" ]; then
|
||||
LOGIN_CMD="docker login $ACR_REGISTRY -u $ACR_USERNAME -p $ACR_PASSWORD"
|
||||
fi
|
||||
|
||||
echo "== 2/2 远端拉取镜像并启动 =="
|
||||
python3 remote_run.py "$HOST" "$PORT" "$USER" <<EOF
|
||||
set -e
|
||||
mkdir -p "$REMOTE_DIR"
|
||||
cd "$REMOTE_DIR"
|
||||
cat > .env <<ENV
|
||||
ACR_REGISTRY=$ACR_REGISTRY
|
||||
ACR_NAMESPACE=$ACR_NAMESPACE
|
||||
VERSION=$VERSION
|
||||
MYSQL_PASSWORD=${MYSQL_PASSWORD:-Cas@123456}
|
||||
MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD:-Root@123456}
|
||||
ENV
|
||||
/bin/chmod +x "$REMOTE_DIR/run-cas.sh"
|
||||
$LOGIN_CMD
|
||||
docker compose -f docker-compose.cas.yml pull cas
|
||||
docker compose -f docker-compose.cas.yml up -d --force-recreate
|
||||
docker image prune -f
|
||||
echo DEPLOY_OK
|
||||
EOF
|
||||
@@ -0,0 +1,85 @@
|
||||
COPIED_OK
|
||||
|
||||
local war size: 173751847
|
||||
|
||||
=== RUNNING WAR LoginController features ===
|
||||
has window.location.href (v1.8 success): True
|
||||
has sendRedirect (error path): True
|
||||
has text/html (v1.8 success): True
|
||||
|
||||
=== RUNNING WAR index.html ===
|
||||
has action=/api/login: True
|
||||
has ?v=1.8: True
|
||||
has name=username: True
|
||||
|
||||
=== container filesystem: any default-redirect-url? ===
|
||||
---env---
|
||||
|
||||
|
||||
=== effective cas.properties (head) ===
|
||||
# CAS prod config (managed in nacos, dataId=cas-prod.properties, group=CAS, namespace=cas)
|
||||
# 去掉 /cas 上下文路径,对外域名走 sso.yunhaixingtong.com
|
||||
cas.server.name: http://sso.yunhaixingtong.com
|
||||
cas.server.prefix: http://sso.yunhaixingtong.com
|
||||
|
||||
cas.adminPagesSecurity.ip: 127\.0\.0\.1
|
||||
|
||||
logging.config: file:/etc/cas/config/log4j2.xml
|
||||
|
||||
server.port: 8443
|
||||
server.context-path: /
|
||||
server.ssl.enabled: true
|
||||
server.ssl.key-store: /etc/cas/cas.keystore
|
||||
server.ssl.key-store-password: changeit
|
||||
server.ssl.key-password: changeit
|
||||
server.ssl.key-alias: cas
|
||||
|
||||
# TGC 经 caddy(HTTP) 透传,关闭 Secure 以便浏览器在 http 下也能存 cookie
|
||||
cas.tgc.secure: false
|
||||
# 上下文路径已改为 /,cookie 作用域必须同步为 /,否则 SSO 票据校验拿不到 TGC
|
||||
cas.tgc.path: /
|
||||
|
||||
# ===== 数据库认证(自定义 DbAuthenticationHandler 读取)=====
|
||||
cas.authn.jdbc.query[0].sql: select * from sys_user where username=?
|
||||
cas.authn.jdbc.query[0].url: jdbc:mysql://mysql:3306/cas?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
|
||||
cas.authn.jdbc.query[0].user: cas
|
||||
cas.authn.jdbc.query[0].password: Cas@123456
|
||||
cas.authn.jdbc.query[0].driverClass: com.mysql.jdbc.Driver
|
||||
cas.authn.jdbc.query[0].fieldPassword: password
|
||||
cas.authn.jdbc.query[0].fieldDisabled: disabled
|
||||
cas.authn.jdbc.query[0].fieldExpired: expired
|
||||
cas.authn.jdbc.query[0].url=jdbc:mysql://mysql8:3306/bsch_userbase?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
|
||||
cas.authn.jdbc.query[0].user=root
|
||||
cas.authn.jdbc.query[0].password=CVf^x$sW*b6CGHxh
|
||||
cas.authn.jdbc.query[0].driverClass=com.mysql.jdbc.Driver
|
||||
spring.redis.host=redis-server
|
||||
spring.redis.port=6379
|
||||
spring.redis.password=wv&WcDYAmLtqTIGM
|
||||
spring.redis.database=0
|
||||
spring.redis.timeout=5000
|
||||
spring.redis.ssl=false
|
||||
spring.redis.jedis.pool.max-active=8
|
||||
spring.redis.jedis.pool.max-idle=8
|
||||
spring.redis.jedis.pool.min-idle=0
|
||||
spring.redis.jedis.pool.max-wait=3000
|
||||
spring.rabbitmq.host=rabbitmq
|
||||
spring.rabbitmq.port=5672
|
||||
spring.rabbitmq.username=admin
|
||||
spring.rabbitmq.password=JdU29I#Q2E4yXoUl
|
||||
spring.rabbitmq.virtual-host=/
|
||||
|
||||
|
||||
=== JDBC url in container ===
|
||||
cas.authn.jdbc.query[0].sql: select * from sys_user where username=?
|
||||
cas.authn.jdbc.query[0].url: jdbc:mysql://mysql:3306/cas?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
|
||||
cas.authn.jdbc.query[0].user: cas
|
||||
cas.authn.jdbc.query[0].password: Cas@123456
|
||||
cas.authn.jdbc.query[0].driverClass: com.mysql.jdbc.Driver
|
||||
cas.authn.jdbc.query[0].fieldPassword: password
|
||||
cas.authn.jdbc.query[0].fieldDisabled: disabled
|
||||
cas.authn.jdbc.query[0].fieldExpired: expired
|
||||
cas.authn.jdbc.query[0].url=jdbc:mysql://mysql8:3306/bsch_userbase?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
|
||||
cas.authn.jdbc.query[0].user=root
|
||||
cas.authn.jdbc.query[0].password=CVf^x$sW*b6CGHxh
|
||||
cas.authn.jdbc.query[0].driverClass=com.mysql.jdbc.Driver
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
# CAS 服务端编排:仅 cas 应用。
|
||||
# MySQL / Redis / RabbitMQ 均为 infra 公共实例(mysql8 等),不在本编排内自建。
|
||||
# CAS 启动时由 run-cas.sh 从 Nacos 拉取:
|
||||
# - 公共基础配置 common.properties (namespace=public)
|
||||
# - CAS 自身配置 cas-prod.properties (namespace=cas)
|
||||
# 启动:cd /opt/docker/apps/cas-sso && docker compose -f docker-compose.cas.yml up -d
|
||||
services:
|
||||
cas:
|
||||
image: ${CAS_IMAGE:-registry.cn-hangzhou.aliyuncs.com/damaomi_docker/yunhai:${VERSION:-v1.3}}
|
||||
container_name: cas-sso
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- infra-infra-net
|
||||
environment:
|
||||
NACOS_ADDR: nacos:8848
|
||||
NACOS_USER: nacos
|
||||
NACOS_PASS: nacos
|
||||
# CAS 自身配置
|
||||
CAS_CONFIG_DATA_ID: cas-prod.properties
|
||||
CAS_CONFIG_GROUP: CAS
|
||||
CAS_CONFIG_NAMESPACE: cas
|
||||
# 公共基础配置(含 mysql / redis / rabbitmq 连接)
|
||||
CAS_BASE_CONFIG_DATA_ID: common.properties
|
||||
CAS_BASE_CONFIG_GROUP: DEFAULT_GROUP
|
||||
CAS_BASE_CONFIG_NAMESPACE: public
|
||||
# 用宿主机(部署目录)最新的 run-cas.sh 覆盖镜像内旧版本,避免每次改脚本都重新构建镜像
|
||||
# 注意:cas.war 不再挂载,直接烤进镜像(由 build-push.sh 重建),避免热修包被 recreate 冲掉/覆盖导致不可预期的问题
|
||||
volumes:
|
||||
- ./run-cas.sh:/app/run-cas.sh:ro
|
||||
ports:
|
||||
- "8443:8443"
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-fk", "https://localhost:8443/login"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
|
||||
networks:
|
||||
infra-infra-net:
|
||||
external: true
|
||||
@@ -0,0 +1,11 @@
|
||||
import paramiko
|
||||
ssh=paramiko.SSHClient(); ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect("192.168.10.157", port=22, username="yunhai", password="123456", timeout=20)
|
||||
def run(c):
|
||||
i,o,e=ssh.exec_command(c); return o.read().decode('utf-8','ignore')+("ERR:"+e.read().decode('utf-8','ignore') if e.read() else "")
|
||||
out=run("docker logs --tail 200 cas-sso 2>&1 | grep -n -A 25 '建立 SSO 会话失败\\|createTicketGrantingTicket\\|AbstractTicketException\\|AuthenticationException\\|Cannot\\|Exception' | tail -60")
|
||||
print(out if out.strip() else "(no exception traces found)")
|
||||
# also check redis ticket registry config
|
||||
print("\n=== redis ticket registry config? ===")
|
||||
print(run("docker exec cas-sso sh -c 'grep -ri ticket.registry /etc/cas/config/cas.properties 2>/dev/null; grep -ri ticket.registry /app/*.properties 2>/dev/null' || echo none"))
|
||||
ssh.close()
|
||||
@@ -0,0 +1,15 @@
|
||||
import paramiko
|
||||
ssh=paramiko.SSHClient(); ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect("192.168.10.157", port=22, username="yunhai", password="123456", timeout=20)
|
||||
def run(c):
|
||||
i,o,e=ssh.exec_command(c); return o.read().decode('utf-8','ignore')+("ERR:"+e.read().decode('utf-8','ignore') if e.read() else "")
|
||||
|
||||
print("=== 1) env vars containing REDIRECT / CUSTOM / CAS_LOGIN ===")
|
||||
print(run("docker exec cas-sso env 2>/dev/null | grep -iE 'REDIRECT|CUSTOM|LOGIN|CAS_SERVER' "))
|
||||
print("\n=== 2) full grep default-redirect-url in container FS ===")
|
||||
print(run("docker exec cas-sso grep -rni 'default-redirect-url' /etc/cas /app 2>/dev/null | head"))
|
||||
print("\n=== 3) run-cas.sh: how is cas.properties built? any hardcode of redirect/cas/login ===")
|
||||
print(run("docker exec cas-sso grep -niE 'redirect|default-redirect|cas/login|cas.properties|NACOS|dataId|cat >' /app/run-cas.sh 2>/dev/null | head -40"))
|
||||
print("\n=== 4) the effective cas.properties custom.* block ===")
|
||||
print(run("docker exec cas-sso sh -c 'grep -niE \"^custom|^cas\" /etc/cas/config/cas.properties 2>/dev/null | head -40'"))
|
||||
ssh.close()
|
||||
@@ -0,0 +1,19 @@
|
||||
-- CAS 专用 MySQL 初始化(仅首次启动执行一次)
|
||||
-- 库/用户由容器环境变量 MYSQL_DATABASE / MYSQL_USER / MYSQL_PASSWORD 创建,这里只建表与种子数据
|
||||
|
||||
CREATE DATABASE IF NOT EXISTS cas CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
USE cas;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sys_user (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(64) NOT NULL,
|
||||
password VARCHAR(100) NOT NULL,
|
||||
disabled TINYINT(1) DEFAULT 0,
|
||||
expired TINYINT(1) DEFAULT 0,
|
||||
UNIQUE KEY uk_username (username)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- 默认管理员:admin / admin123 (BCrypt,前缀 $2a$ 与 Spring Security 兼容)
|
||||
INSERT INTO sys_user(username, password, disabled, expired) VALUES
|
||||
('admin', '$2a$10$kRc0qMMC9NGbE.46fEWZvOlpcy.pjKef2ZA9yoFdKFfBw.nEJEUZO', 0, 0)
|
||||
ON DUPLICATE KEY UPDATE password = VALUES(password);
|
||||
@@ -0,0 +1,38 @@
|
||||
import zipfile, os, shutil
|
||||
|
||||
PROJ = "D:/SoftProject/yunhai/CasSso/cas-overlay-template-5.3"
|
||||
WAR = os.path.join(PROJ, "docker/cas.war")
|
||||
|
||||
# (entry_name_in_war, local_source_file)
|
||||
REPL = [
|
||||
("WEB-INF/classes/org/apereo/cas/web/custom/controller/LoginController.class",
|
||||
os.path.join(PROJ, "target/cas/WEB-INF/classes/org/apereo/cas/web/custom/controller/LoginController.class")),
|
||||
("WEB-INF/classes/org/apereo/cas/web/custom/controller/LoginController$LocalAuthenticationResult.class",
|
||||
os.path.join(PROJ, "target/cas/WEB-INF/classes/org/apereo/cas/web/custom/controller/LoginController$LocalAuthenticationResult.class")),
|
||||
("front/index.html",
|
||||
os.path.join(PROJ, "src/main/webapp/front/index.html")),
|
||||
]
|
||||
|
||||
repl_map = {name: open(src, "rb").read() for name, src in REPL}
|
||||
|
||||
tmp = WAR + ".tmp"
|
||||
zin = zipfile.ZipFile(WAR)
|
||||
zout = zipfile.ZipFile(tmp, "w", zipfile.ZIP_DEFLATED)
|
||||
for item in zin.infolist():
|
||||
if item.filename in repl_map:
|
||||
zout.writestr(item, repl_map[item.filename])
|
||||
else:
|
||||
zout.writestr(item, zin.read(item.filename))
|
||||
zin.close()
|
||||
zout.close()
|
||||
shutil.move(tmp, WAR)
|
||||
|
||||
z = zipfile.ZipFile(WAR)
|
||||
print("testzip:", z.testzip())
|
||||
html = z.read("front/index.html").decode("utf-8", "replace")
|
||||
print("has pwdMgrForm:", "pwdMgrForm" in html)
|
||||
print("has name=\"username\":", 'name="username"' in html)
|
||||
print("has name=\"password\":", 'name="password"' in html)
|
||||
cls = z.read("WEB-INF/classes/org/apereo/cas/web/custom/controller/LoginController.class")
|
||||
print("default url in class:", b"front/index.html" in cls)
|
||||
print("entries replaced OK")
|
||||
@@ -0,0 +1,47 @@
|
||||
import zipfile, os, shutil
|
||||
|
||||
WAR = "docker/cas.war"
|
||||
CLS_DIR = "target/cas/WEB-INF/classes/org/apereo/cas/web/custom/controller"
|
||||
SRC_INDEX = "src/main/webapp/front/index.html"
|
||||
IDX_ENTRY = "front/index.html"
|
||||
|
||||
files = {
|
||||
os.path.join(CLS_DIR, "LoginController.class"): "WEB-INF/classes/org/apereo/cas/web/custom/controller/LoginController.class",
|
||||
os.path.join(CLS_DIR, "LoginController$LocalAuthenticationResult.class"): "WEB-INF/classes/org/apereo/cas/web/custom/controller/LoginController$LocalAuthenticationResult.class",
|
||||
SRC_INDEX: IDX_ENTRY,
|
||||
"src/main/resources/application.yml": "WEB-INF/classes/application.yml",
|
||||
}
|
||||
|
||||
tmp = WAR + ".tmp"
|
||||
zin = zipfile.ZipFile(WAR)
|
||||
zout = zipfile.ZipFile(tmp, "w", zipfile.ZIP_DEFLATED)
|
||||
for item in zin.infolist():
|
||||
if item.filename in files.values():
|
||||
continue
|
||||
zout.writestr(item, zin.read(item.filename))
|
||||
zin.close()
|
||||
for src, entry in files.items():
|
||||
data = open(src, "rb").read()
|
||||
# 用原条目元数据保持权限/时间戳
|
||||
zi = zipfile.ZipInfo(entry)
|
||||
zi.date_time = (2026, 8, 3, 18, 10, 0)
|
||||
zi.compress_type = zipfile.ZIP_DEFLATED
|
||||
zi.external_attr = 0o644 << 16
|
||||
zout.writestr(zi, data)
|
||||
zout.close()
|
||||
os.replace(tmp, WAR)
|
||||
|
||||
# 校验
|
||||
z = zipfile.ZipFile(WAR)
|
||||
print("testzip:", z.testzip())
|
||||
idx = z.read(IDX_ENTRY).decode("utf-8", "replace")
|
||||
cls = z.read("WEB-INF/classes/org/apereo/cas/web/custom/controller/LoginController.class").decode("latin1")
|
||||
yml = z.read("WEB-INF/classes/application.yml").decode("utf-8", "replace")
|
||||
print("index.html has ?v=1.8:", "?v=1.8" in idx)
|
||||
print("index.html has real form action /api/login:", 'action="/api/login"' in idx)
|
||||
print("index.html has loginname hidden:", 'name="loginname"' in idx)
|
||||
print("class has window.location.href (v1.8 success):", "window.location.href" in cls)
|
||||
print("class has guard /cas/login fallback:", 'contains("/cas/login")' in cls)
|
||||
yml_ok = "default-redirect-url: /front/index.html" in yml and "default-redirect-url: /cas/login" not in yml
|
||||
print("application.yml default-redirect-url fixed (/front/index.html):", yml_ok)
|
||||
print("WAR size:", os.path.getsize(WAR))
|
||||
@@ -0,0 +1,51 @@
|
||||
import urllib.request, ssl, paramiko, json, base64, bcrypt, socket
|
||||
from cryptography.hazmat.primitives.serialization import load_der_public_key
|
||||
from cryptography.hazmat.primitives.asymmetric import padding
|
||||
|
||||
BASE="http://sso.yunhaixingtong.com"; HOST="sso.yunhaixingtong.com"; PORT=80
|
||||
ctx=ssl.create_default_context(); ctx.check_hostname=False; ctx.verify_mode=ssl.CERT_NONE
|
||||
PLAIN="SsoTest@2026"
|
||||
h=bcrypt.hashpw(PLAIN.encode(), bcrypt.gensalt(rounds=10)).decode()
|
||||
if h.startswith("$2b$"): h=h.replace("$2b$","$2a$",1)
|
||||
USER="ssotest_tmp"
|
||||
|
||||
ssh=paramiko.SSHClient(); ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect("192.168.10.157", port=22, username="yunhai", password="123456", timeout=20)
|
||||
def run(c):
|
||||
i,o,e=ssh.exec_command(c); return o.read().decode('utf-8','ignore')+("ERR:"+e.read().decode('utf-8','ignore') if e.read() else "")
|
||||
PWD="CVf^x$sW*b6CGHxh"
|
||||
|
||||
# write SQL via sftp (avoid shell $ expansion of the BCrypt hash)
|
||||
seed=f"DELETE FROM sys_user WHERE username='{USER}';\nINSERT INTO sys_user(username,create_time,create_user,password,disabled,expired) VALUES('{USER}',NOW(),0,'{h}',0,0);\n"
|
||||
sftp=ssh.open_sftp(); sftp.open('/tmp/seed.sql','w').write(seed); sftp.close()
|
||||
print("seed.sql:\n", seed)
|
||||
print("import result:", run(f"docker exec -i mysql8 mysql -uroot -p'{PWD}' bsch_userbase < /tmp/seed.sql 2>&1").strip())
|
||||
|
||||
pub=json.loads(urllib.request.urlopen(urllib.request.Request(BASE+"/api/public-key"),timeout=20,context=ctx).read().decode())["data"]["publicKey"]
|
||||
rsa_pub=load_der_public_key(base64.b64decode(pub))
|
||||
def rsa_enc(s): return base64.b64encode(rsa_pub.encrypt(s.encode(), padding.PKCS1v15())).decode()
|
||||
|
||||
def raw_post(extra=None):
|
||||
body=f"loginname={rsa_enc(USER)}&pwd={rsa_enc(PLAIN)}"
|
||||
hh={"Host":HOST,"Content-Type":"application/x-www-form-urlencoded","Content-Length":str(len(body))}
|
||||
if extra: hh.update(extra)
|
||||
req="POST /api/login HTTP/1.1\r\n"+"\r\n".join(f"{k}: {v}" for k,v in hh.items())+"\r\nConnection: close\r\n\r\n"+body
|
||||
s=socket.create_connection((HOST,PORT),timeout=20); s.sendall(req.encode()); data=b""
|
||||
while True:
|
||||
c=s.recv(4096)
|
||||
if not c: break
|
||||
data+=c
|
||||
s.close(); return data.decode('utf-8','ignore')
|
||||
|
||||
print("\n=== SUCCESS login, NATIVE FORM (no XHR) ===")
|
||||
r=raw_post(); print(r.split("\r\n\r\n",1)[0][:1000]); print("BODY:", r.split("\r\n\r\n",1)[1][:300] if "\r\n\r\n" in r else "(no body)")
|
||||
|
||||
print("\n=== SUCCESS login, AJAX (X-Requested-With) ===")
|
||||
r2=raw_post({"X-Requested-With":"XMLHttpRequest","Accept":"application/json"})
|
||||
print(r2.split("\r\n\r\n",1)[0][:400]); print("BODY:", r2.split("\r\n\r\n",1)[1][:200] if "\r\n\r\n" in r2 else "")
|
||||
|
||||
# cleanup
|
||||
sftp=ssh.open_sftp(); sftp.open('/tmp/del.sql','w').write(f"DELETE FROM sys_user WHERE username='{USER}';\n"); sftp.close()
|
||||
run(f"docker exec -i mysql8 mysql -uroot -p'{PWD}' bsch_userbase < /tmp/del.sql 2>&1")
|
||||
print("\ncleanup done")
|
||||
ssh.close()
|
||||
@@ -0,0 +1,65 @@
|
||||
import urllib.request, ssl, paramiko, json, base64, bcrypt
|
||||
from cryptography.hazmat.primitives.serialization import load_der_public_key
|
||||
from cryptography.hazmat.primitives.asymmetric import padding
|
||||
|
||||
BASE="http://sso.yunhaixingtong.com"
|
||||
HOST="sso.yunhaixingtong.com"; PORT=80
|
||||
ctx=ssl.create_default_context(); ctx.check_hostname=False; ctx.verify_mode=ssl.CERT_NONE
|
||||
|
||||
# ---- 1) known password + BCrypt hash (strength 10, prefix $2a$ to match Spring) ----
|
||||
PLAIN="SsoTest@2026"
|
||||
h=bcrypt.hashpw(PLAIN.encode(), bcrypt.gensalt(rounds=10)).decode()
|
||||
if h.startswith("$2b$"): h=h.replace("$2b$","$2a$",1)
|
||||
print("BCrypt hash:", h)
|
||||
|
||||
# ---- 2) insert temp user into bsch_userbase ----
|
||||
ssh=paramiko.SSHClient(); ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect("192.168.10.157", port=22, username="yunhai", password="123456", timeout=20)
|
||||
def run(c):
|
||||
i,o,e=ssh.exec_command(c); return o.read().decode('utf-8','ignore')+("ERR:"+e.read().decode('utf-8','ignore') if e.read() else "")
|
||||
PWD="CVf^x$sW*b6CGHxh"
|
||||
USER="ssotest_tmp"
|
||||
run(f"docker exec mysql8 mysql -uroot -p'{PWD}' bsch_userbase -e \"DELETE FROM sys_user WHERE username='{USER}';\" 2>&1")
|
||||
ins=run(f"docker exec mysql8 mysql -uroot -p'{PWD}' bsch_userbase -e \"INSERT INTO sys_user(username,create_time,create_user,password,disabled,expired) VALUES('{USER}',NOW(),0,'{h}',0,0);\" 2>&1")
|
||||
print("INSERT result:", ins.strip())
|
||||
|
||||
# ---- 3) RSA public key ----
|
||||
pub=json.loads(urllib.request.urlopen(urllib.request.Request(BASE+"/api/public-key"),timeout=20,context=ctx).read().decode())["data"]["publicKey"]
|
||||
der=base64.b64decode(pub)
|
||||
rsa_pub=load_der_public_key(der)
|
||||
def rsa_enc(s):
|
||||
return base64.b64encode(rsa_pub.encrypt(s.encode(), padding.PKCS1v15())).decode()
|
||||
|
||||
# ---- 4) raw socket POST (native form, NO X-Requested-With) ----
|
||||
def raw_post(headers_extra=None):
|
||||
body=f"loginname={rsa_enc(USER)}&pwd={rsa_enc(PLAIN)}"
|
||||
h={"Host":HOST,"Content-Type":"application/x-www-form-urlencoded",
|
||||
"Content-Length":str(len(body))}
|
||||
if headers_extra: h.update(headers_extra)
|
||||
req="POST /api/login HTTP/1.1\r\n"+"\r\n".join(f"{k}: {v}" for k,v in h.items())+"\r\nConnection: close\r\n\r\n"+body
|
||||
s=socket_create(); s.sendall(req.encode())
|
||||
data=b""
|
||||
while True:
|
||||
c=s.recv(4096)
|
||||
if not c: break
|
||||
data+=c
|
||||
s.close()
|
||||
return data.decode('utf-8','ignore')
|
||||
|
||||
import socket
|
||||
def socket_create(): return socket.create_connection((HOST,PORT),timeout=20)
|
||||
|
||||
print("\n=== SUCCESS login, NATIVE FORM (no XHR) -> expect 200 HTML w/ window.location.href ===")
|
||||
resp=raw_post()
|
||||
head=resp.split("\r\n\r\n",1)[0]
|
||||
print(head[:1200])
|
||||
|
||||
print("\n=== SUCCESS login, AJAX (X-Requested-With) -> expect JSON {code:0,data:{url}} ===")
|
||||
resp2=raw_post({"X-Requested-With":"XMLHttpRequest","Accept":"application/json"})
|
||||
head2=resp2.split("\r\n\r\n",1)[0]
|
||||
print(head2[:800])
|
||||
|
||||
# cleanup
|
||||
run(f"docker exec mysql8 mysql -uroot -p'{PWD}' bsch_userbase -e \"DELETE FROM sys_user WHERE username='{USER}';\" 2>&1")
|
||||
print("\ncleanup done")
|
||||
ssh.close()
|
||||
@@ -0,0 +1,60 @@
|
||||
import urllib.request, json, base64, ssl
|
||||
from cryptography.hazmat.primitives.asymmetric import padding as asym_padding
|
||||
from cryptography.hazmat.primitives.serialization import load_pem_public_key
|
||||
|
||||
HOST_HDR = "sso.yunhaixingtong.com"
|
||||
BASE = "http://192.168.10.157:80"
|
||||
|
||||
def req(method, path, data=None, headers=None):
|
||||
h = {"Host": HOST_HDR}
|
||||
if headers: h.update(headers)
|
||||
if data is not None:
|
||||
data = urllib.parse.urlencode(data).encode()
|
||||
h["Content-Type"] = "application/x-www-form-urlencoded"
|
||||
r = urllib.request.Request(BASE+path, data=data, headers=h, method=method)
|
||||
try:
|
||||
resp = urllib.request.urlopen(r, timeout=20)
|
||||
body = resp.read().decode("utf-8","replace")
|
||||
return resp.status, dict(resp.getheaders()), body
|
||||
except urllib.error.HTTPError as e:
|
||||
return e.code, dict(e.headers), e.read().decode("utf-8","replace")
|
||||
|
||||
# 1) 取公钥
|
||||
st, hd, body = req("GET", "/api/public-key")
|
||||
print("=== /api/public-key ===")
|
||||
print("status", st)
|
||||
print("body", body[:600])
|
||||
pk = json.loads(body)["data"]["publicKey"]
|
||||
print("publicKey len", len(pk))
|
||||
print("publicKey head:", pk[:60])
|
||||
|
||||
# 重组为合法 PEM(端点把空白去掉了,补回换行)
|
||||
b64 = pk.replace("-----BEGIN PUBLIC KEY-----","").replace("-----END PUBLIC KEY-----","").replace("-----BEGIN RSA PUBLIC KEY-----","").replace("-----END RSA PUBLIC KEY-----","").replace("\n","").replace(" ","")
|
||||
pem = "-----BEGIN PUBLIC KEY-----\n" + "\n".join(b64[i:i+64] for i in range(0,len(b64),64)) + "\n-----END PUBLIC KEY-----"
|
||||
pub = load_pem_public_key(pem.encode())
|
||||
|
||||
def enc(pt):
|
||||
ct = pub.encrypt(pt.encode("utf-8"), asym_padding.PKCS1v15())
|
||||
return base64.b64encode(ct).decode()
|
||||
|
||||
# 用户名/密码:用已知可登录的 yunhaisch(密码需正确才能成功;若不知道正确密码,可先看错误分支)
|
||||
USERNAME = "yunhaisch"
|
||||
PASSWORD = input("请输入 yunhaisch 的密码(仅本地测试用,不会外发): ").strip()
|
||||
|
||||
euser = enc(USERNAME)
|
||||
epwd = enc(PASSWORD)
|
||||
|
||||
# 2) 登录(不带 service)
|
||||
st, hd, body = req("POST", "/api/login", data={
|
||||
"loginname": euser, "pwd": epwd
|
||||
})
|
||||
print("\n=== POST /api/login (无 service) ===")
|
||||
print("status", st)
|
||||
print("Set-Cookie:", hd.get("Set-Cookie"))
|
||||
print("body", body[:800])
|
||||
try:
|
||||
j = json.loads(body)
|
||||
print(">>> code=", j.get("code"), "msg=", j.get("msg"))
|
||||
print(">>> data.url=", (j.get("data") or {}).get("url"))
|
||||
except Exception as e:
|
||||
print("parse err", e)
|
||||
@@ -0,0 +1,50 @@
|
||||
import urllib.request, ssl, paramiko, json, base64, bcrypt, socket
|
||||
from cryptography.hazmat.primitives.serialization import load_der_public_key
|
||||
from cryptography.hazmat.primitives.asymmetric import padding
|
||||
|
||||
BASE="http://sso.yunhaixingtong.com"; HOST="sso.yunhaixingtong.com"; PORT=80
|
||||
ctx=ssl.create_default_context(); ctx.check_hostname=False; ctx.verify_mode=ssl.CERT_NONE
|
||||
PLAIN="SsoTest@2026"
|
||||
h=bcrypt.hashpw(PLAIN.encode(), bcrypt.gensalt(rounds=10)).decode()
|
||||
if h.startswith("$2b$"): h=h.replace("$2b$","$2a$",1)
|
||||
USER="ssotest_tmp"
|
||||
UA="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36"
|
||||
|
||||
ssh=paramiko.SSHClient(); ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect("192.168.10.157", port=22, username="yunhai", password="123456", timeout=20)
|
||||
def run(c):
|
||||
i,o,e=ssh.exec_command(c); return o.read().decode('utf-8','ignore')+("ERR:"+e.read().decode('utf-8','ignore') if e.read() else "")
|
||||
PWD="CVf^x$sW*b6CGHxh"
|
||||
seed=f"DELETE FROM sys_user WHERE username='{USER}';\nINSERT INTO sys_user(username,create_time,create_user,password,disabled,expired) VALUES('{USER}',NOW(),0,'{h}',0,0);\n"
|
||||
sftp=ssh.open_sftp(); sftp.open('/tmp/seed.sql','w').write(seed); sftp.close()
|
||||
run(f"docker exec -i mysql8 mysql -uroot -p'{PWD}' bsch_userbase < /tmp/seed.sql 2>&1")
|
||||
|
||||
pub=json.loads(urllib.request.urlopen(urllib.request.Request(BASE+"/api/public-key"),timeout=20,context=ctx).read().decode())["data"]["publicKey"]
|
||||
rsa_pub=load_der_public_key(base64.b64decode(pub))
|
||||
def rsa_enc(s): return base64.b64encode(rsa_pub.encrypt(s.encode(), padding.PKCS1v15())).decode()
|
||||
|
||||
def raw_post(extra=None):
|
||||
body=f"loginname={rsa_enc(USER)}&pwd={rsa_enc(PLAIN)}"
|
||||
hh={"Host":HOST,"User-Agent":UA,"Content-Type":"application/x-www-form-urlencoded","Content-Length":str(len(body))}
|
||||
if extra: hh.update(extra)
|
||||
req="POST /api/login HTTP/1.1\r\n"+"\r\n".join(f"{k}: {v}" for k,v in hh.items())+"\r\nConnection: close\r\n\r\n"+body
|
||||
s=socket.create_connection((HOST,PORT),timeout=20); s.sendall(req.encode()); data=b""
|
||||
while True:
|
||||
c=s.recv(4096)
|
||||
if not c: break
|
||||
data+=c
|
||||
s.close(); return data.decode('utf-8','ignore')
|
||||
|
||||
print("=== SUCCESS login w/ User-Agent, NATIVE FORM ===")
|
||||
r=raw_post()
|
||||
print("HEAD:\n", r.split("\r\n\r\n",1)[0][:700])
|
||||
print("BODY:\n", r.split("\r\n\r\n",1)[1][:400] if "\r\n\r\n" in r else "(no body)")
|
||||
|
||||
print("\n=== SUCCESS login w/ User-Agent, AJAX ===")
|
||||
r2=raw_post({"X-Requested-With":"XMLHttpRequest","Accept":"application/json"})
|
||||
print("HEAD:\n", r2.split("\r\n\r\n",1)[0][:400])
|
||||
print("BODY:\n", r2.split("\r\n\r\n",1)[1][:300] if "\r\n\r\n" in r2 else "")
|
||||
|
||||
sftp=ssh.open_sftp(); sftp.open('/tmp/del.sql','w').write(f"DELETE FROM sys_user WHERE username='{USER}';\n"); sftp.close()
|
||||
run(f"docker exec -i mysql8 mysql -uroot -p'{PWD}' bsch_userbase < /tmp/del.sql 2>&1")
|
||||
print("\ncleanup done"); ssh.close()
|
||||
@@ -0,0 +1,37 @@
|
||||
import urllib.request, urllib.error, ssl
|
||||
|
||||
BASE = "http://sso.yunhaixingtong.com"
|
||||
ctx = ssl.create_default_context()
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_mode = ssl.CERT_NONE
|
||||
|
||||
def req(method, path, data=None, headers=None):
|
||||
url = BASE + path
|
||||
h = headers or {}
|
||||
r = urllib.request.Request(url, data=data, method=method, headers=h)
|
||||
try:
|
||||
resp = urllib.request.urlopen(r, timeout=20, context=ctx)
|
||||
body = resp.read().decode('utf-8', 'ignore')
|
||||
return resp.status, dict(resp.getheaders()), body
|
||||
except urllib.error.HTTPError as e:
|
||||
body = e.read().decode('utf-8', 'ignore')
|
||||
return e.code, dict(e.headers), body
|
||||
|
||||
print("=== 1) /front/index.html WITHOUT auth (is it behind CAS?) ===")
|
||||
st, hd, body = req("GET", "/front/index.html")
|
||||
print("status:", st, "| Location:", hd.get("Location"))
|
||||
print("body has /cas/login:", "/cas/login" in body, "| ?v=1.7:", "v=1.7" in body,
|
||||
"| action=/api/login:", 'action="/api/login"' in body, "| name=username:", 'name="username"' in body)
|
||||
|
||||
print("\n=== 2) POST /api/login BOGUS (native form, no XHR header) ===")
|
||||
form = "loginname=AAA&pwd=BBB&captcha=".encode()
|
||||
st, hd, body = req("POST", "/api/login", data=form, headers={"Content-Type":"application/x-www-form-urlencoded"})
|
||||
print("status:", st, "| Location:", hd.get("Location"))
|
||||
|
||||
print("\n=== 3) POST /api/login BOGUS (AJAX X-Requested-With) ===")
|
||||
st, hd, body = req("POST", "/api/login", data=form, headers={"Content-Type":"application/x-www-form-urlencoded","X-Requested-With":"XMLHttpRequest"})
|
||||
print("status:", st, "| body[:180]:", body[:180])
|
||||
|
||||
print("\n=== 4) /api/public-key ===")
|
||||
st, hd, body = req("GET", "/api/public-key")
|
||||
print("status:", st, "| body[:160]:", body[:160])
|
||||
@@ -0,0 +1,39 @@
|
||||
import urllib.request, ssl, paramiko, json, base64, bcrypt, socket
|
||||
from cryptography.hazmat.primitives.serialization import load_der_public_key
|
||||
from cryptography.hazmat.primitives.asymmetric import padding
|
||||
|
||||
BASE="http://sso.yunhaixingtong.com"; HOST="sso.yunhaixingtong.com"; PORT=80
|
||||
ctx=ssl.create_default_context(); ctx.check_hostname=False; ctx.verify_mode=ssl.CERT_NONE
|
||||
PLAIN="SsoTest@2026"
|
||||
h=bcrypt.hashpw(PLAIN.encode(), bcrypt.gensalt(rounds=10)).decode()
|
||||
if h.startswith("$2b$"): h=h.replace("$2b$","$2a$",1)
|
||||
print("hash self-check (python):", bcrypt.checkpw(PLAIN.encode(), h.encode()))
|
||||
|
||||
ssh=paramiko.SSHClient(); ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect("192.168.10.157", port=22, username="yunhai", password="123456", timeout=20)
|
||||
def run(c):
|
||||
i,o,e=ssh.exec_command(c); return o.read().decode('utf-8','ignore')+("ERR:"+e.read().decode('utf-8','ignore') if e.read() else "")
|
||||
PWD="CVf^x$sW*b6CGHxh"; USER="ssotest_tmp"
|
||||
run(f"docker exec mysql8 mysql -uroot -p'{PWD}' bsch_userbase -e \"DELETE FROM sys_user WHERE username='{USER}'; INSERT INTO sys_user(username,create_time,create_user,password,disabled,expired) VALUES('{USER}',NOW(),0,'{h}',0,0);\" 2>&1")
|
||||
|
||||
pub=json.loads(urllib.request.urlopen(urllib.request.Request(BASE+"/api/public-key"),timeout=20,context=ctx).read().decode())["data"]["publicKey"]
|
||||
rsa_pub=load_der_public_key(base64.b64decode(pub))
|
||||
def rsa_enc(s): return base64.b64encode(rsa_pub.encrypt(s.encode(), padding.PKCS1v15())).decode()
|
||||
|
||||
body=f"loginname={rsa_enc(USER)}&pwd={rsa_enc(PLAIN)}"
|
||||
req="POST /api/login HTTP/1.1\r\nHost: %s\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: %d\r\nConnection: close\r\n\r\n%s"%(HOST,len(body),body)
|
||||
s=socket.create_connection((HOST,PORT),timeout=20); s.sendall(req.encode())
|
||||
data=b""
|
||||
while True:
|
||||
c=s.recv(4096)
|
||||
if not c: break
|
||||
data+=c
|
||||
s.close()
|
||||
print("\nRESPONSE HEAD:\n", data.decode('utf-8','ignore').split("\r\n\r\n",1)[0][:600])
|
||||
|
||||
# fetch logs mentioning ssotest_tmp
|
||||
print("\n=== CAS logs (ssotest_tmp) ===")
|
||||
logs=run("docker logs --tail 60 cas-sso 2>&1 | grep -i 'ssotest_tmp\\|尝试登录\\|密码错误\\|Padding\\|Decryption\\|Illegal' | tail -20")
|
||||
print(logs if logs.strip() else "(no matching lines)")
|
||||
run(f"docker exec mysql8 mysql -uroot -p'{PWD}' bsch_userbase -e \"DELETE FROM sys_user WHERE username='{USER}';\" 2>&1")
|
||||
ssh.close()
|
||||
@@ -0,0 +1,22 @@
|
||||
import urllib.request, ssl, paramiko, json
|
||||
|
||||
BASE="http://sso.yunhaixingtong.com"
|
||||
ctx=ssl.create_default_context(); ctx.check_hostname=False; ctx.verify_mode=ssl.CERT_NONE
|
||||
|
||||
# 1) live RSA public key
|
||||
r=urllib.request.urlopen(urllib.request.Request(BASE+"/api/public-key"), timeout=20, context=ctx)
|
||||
pub=json.loads(r.read().decode())["data"]["publicKey"]
|
||||
print("PUBLIC_KEY_LEN:", len(pub))
|
||||
print("PUBLIC_KEY_HEAD:", pub[:40], "...")
|
||||
|
||||
# 2) sys_user schema + sample usernames (no passwords)
|
||||
ssh=paramiko.SSHClient(); ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect("192.168.10.157", port=22, username="yunhai", password="123456", timeout=20)
|
||||
def run(c):
|
||||
i,o,e=ssh.exec_command(c); return o.read().decode('utf-8','ignore')+("ERR:"+e.read().decode('utf-8','ignore') if e.read() else "")
|
||||
PWD="CVf^x$sW*b6CGHxh"
|
||||
print("\n=== DESCRIBE sys_user ===")
|
||||
print(run(f"docker exec mysql8 mysql -uroot -p'{PWD}' bsch_userbase -e 'DESCRIBE sys_user;' 2>&1 | head -40"))
|
||||
print("\n=== sample usernames (limit 10) ===")
|
||||
print(run(f"docker exec mysql8 mysql -uroot -p'{PWD}' bsch_userbase -e 'SELECT username, disabled, expired FROM sys_user LIMIT 10;' 2>&1 | head -20"))
|
||||
ssh.close()
|
||||
@@ -0,0 +1,58 @@
|
||||
import urllib.request, ssl, paramiko, json, base64, bcrypt, socket, time, re
|
||||
from cryptography.hazmat.primitives.serialization import load_der_public_key
|
||||
from cryptography.hazmat.primitives.asymmetric import padding
|
||||
|
||||
BASE="http://sso.yunhaixingtong.com"; HOST="sso.yunhaixingtong.com"; PORT=80
|
||||
ctx=ssl.create_default_context(); ctx.check_hostname=False; ctx.verify_mode=ssl.CERT_NONE
|
||||
PLAIN="SsoTest@2026"
|
||||
h=bcrypt.hashpw(PLAIN.encode(), bcrypt.gensalt(rounds=10)).decode()
|
||||
if h.startswith("$2b$"): h=h.replace("$2b$","$2a$",1)
|
||||
USER="ssotest_tmp"; UA="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0 Safari/537.36"
|
||||
|
||||
ssh=paramiko.SSHClient(); ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect("192.168.10.157", port=22, username="yunhai", password="123456", timeout=20)
|
||||
def run(c):
|
||||
i,o,e=ssh.exec_command(c); out=o.read().decode('utf-8','ignore'); err=e.read().decode('utf-8','ignore'); return out+("ERR:"+err if err else "")
|
||||
PWD="CVf^x$sW*b6CGHxh"
|
||||
seed=f"DELETE FROM sys_user WHERE username='{USER}'; INSERT INTO sys_user(username,create_time,create_user,password,disabled,expired) VALUES('{USER}',NOW(),0,'{h}',0,0);"
|
||||
sftp=ssh.open_sftp(); sftp.open('/tmp/seed.sql','w').write(seed); sftp.close()
|
||||
print("seed:", run(f"docker exec -i mysql8 mysql -uroot -p'{PWD}' bsch_userbase < /tmp/seed.sql 2>&1").strip())
|
||||
|
||||
def raw_post(extra=None):
|
||||
pub=json.loads(urllib.request.urlopen(urllib.request.Request(BASE+"/api/public-key"),timeout=20,context=ctx).read().decode())["data"]["publicKey"]
|
||||
rsa_pub=load_der_public_key(base64.b64decode(pub))
|
||||
def enc(s): return base64.b64encode(rsa_pub.encrypt(s.encode(), padding.PKCS1v15())).decode()
|
||||
body=f"loginname={enc(USER)}&pwd={enc(PLAIN)}"
|
||||
hh={"Host":HOST,"User-Agent":UA,"Content-Type":"application/x-www-form-urlencoded","Content-Length":str(len(body))}
|
||||
if extra: hh.update(extra)
|
||||
req="POST /api/login HTTP/1.1\r\n"+"\r\n".join(f"{k}: {v}" for k,v in hh.items())+"\r\nConnection: close\r\n\r\n"+body
|
||||
s=socket.create_connection((HOST,PORT),timeout=20); s.sendall(req.encode()); data=b""
|
||||
while True:
|
||||
c=s.recv(4096)
|
||||
if not c: break
|
||||
data+=c
|
||||
s.close(); return data.decode('utf-8','ignore')
|
||||
|
||||
for _ in range(20):
|
||||
try:
|
||||
r=raw_post({"X-Requested-With":"XMLHttpRequest","Accept":"application/json"})
|
||||
if "200 OK" in r.split("\r\n\r\n",1)[0]: break
|
||||
except Exception: pass
|
||||
time.sleep(6)
|
||||
|
||||
print("\n=== AJAX (what the page SHOULD use) ===")
|
||||
r=raw_post({"X-Requested-With":"XMLHttpRequest","Accept":"application/json"})
|
||||
body=r.split("\r\n\r\n",1)[1] if "\r\n\r\n" in r else ""
|
||||
print("RAW BODY:", body[:300])
|
||||
m=re.search(r'"url"\s*:\s*"([^"]*)"', body)
|
||||
print("-> data.url:", m.group(1) if m else "NONE / NOT PRESENT")
|
||||
|
||||
print("\n=== NATIVE FORM (what index.html currently does) ===")
|
||||
r2=raw_post()
|
||||
head=r2.split("\r\n\r\n",1)[0]; body2=r2.split("\r\n\r\n",1)[1] if "\r\n\r\n" in r2 else ""
|
||||
print("STATUS LINE:", head.split("\r\n",1)[0])
|
||||
print("BODY[:400]:", body2[:400])
|
||||
|
||||
sftp=ssh.open_sftp(); sftp.open('/tmp/del.sql','w').write(f"DELETE FROM sys_user WHERE username='{USER}';\n"); sftp.close()
|
||||
print("cleanup:", run(f"docker exec -i mysql8 mysql -uroot -p'{PWD}' bsch_userbase < /tmp/del.sql 2>&1").strip())
|
||||
ssh.close()
|
||||
@@ -0,0 +1,25 @@
|
||||
import socket
|
||||
HOST="sso.yunhaixingtong.com"; PORT=80
|
||||
body="loginname=AAA&pwd=BBB&captcha="
|
||||
req=(
|
||||
"POST /api/login HTTP/1.1\r\n"
|
||||
f"Host: {HOST}\r\n"
|
||||
"Content-Type: application/x-www-form-urlencoded\r\n"
|
||||
f"Content-Length: {len(body)}\r\n"
|
||||
"Connection: close\r\n"
|
||||
"\r\n"
|
||||
+ body
|
||||
)
|
||||
s=socket.create_connection((HOST,PORT),timeout=20)
|
||||
s.sendall(req.encode())
|
||||
data=b""
|
||||
while True:
|
||||
chunk=s.recv(4096)
|
||||
if not chunk: break
|
||||
data+=chunk
|
||||
s.close()
|
||||
text=data.decode('utf-8','ignore')
|
||||
# print status line + headers (up to first blank line)
|
||||
head=text.split("\r\n\r\n",1)[0]
|
||||
print("=== RAW RESPONSE HEAD (native form POST) ===")
|
||||
print(head[:1500])
|
||||
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env python3
|
||||
# 通过 SSH 在远端以 sudo 执行脚本(从 stdin 读取)
|
||||
# 用法: python3 remote_run.py <host> <port> <user> < script.sh
|
||||
# 依赖环境变量 SSHPASS
|
||||
import os, sys, paramiko
|
||||
|
||||
def main():
|
||||
host = sys.argv[1]
|
||||
port = int(sys.argv[2]) if len(sys.argv) > 2 else 22
|
||||
user = sys.argv[3] if len(sys.argv) > 3 else "root"
|
||||
pw = os.environ.get("SSHPASS")
|
||||
if not pw:
|
||||
sys.stderr.write("ERROR: 请先设置环境变量 SSHPASS\n")
|
||||
sys.exit(1)
|
||||
script = sys.stdin.read()
|
||||
c = paramiko.SSHClient()
|
||||
c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
c.connect(host, port, user, password=pw, timeout=30)
|
||||
stdin, stdout, stderr = c.exec_command("sudo -S bash -s", timeout=1800)
|
||||
stdin.write(pw + "\n")
|
||||
stdin.write(script)
|
||||
stdin.flush()
|
||||
stdin.channel.shutdown_write()
|
||||
sys.stdout.write(stdout.read().decode(errors="replace"))
|
||||
err = stderr.read().decode(errors="replace")
|
||||
if err.strip():
|
||||
sys.stderr.write("STDERR:\n" + err)
|
||||
c.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env python3
|
||||
# 将重编译后的 LoginController.class + 修复后的前端文件注入好包 target/cas.war
|
||||
# 其余条目原样保留,确保 WAR 结构/压缩方式不变。
|
||||
import os, zipfile, shutil
|
||||
|
||||
ROOT = r"D:\SoftProject\yunhai\CasSso\cas-overlay-template-5.3"
|
||||
BASE = os.path.join(ROOT, "target", "cas.war")
|
||||
NEW = os.path.join(ROOT, "target", "cas.war.new")
|
||||
|
||||
CLASSES_DIR = os.path.join(ROOT, "target", "cas", "WEB-INF", "classes",
|
||||
"org", "apereo", "cas", "web", "custom", "controller")
|
||||
FRONT_JS = os.path.join(ROOT, "src", "main", "webapp", "front", "js", "conf.js")
|
||||
FRONT_IDX = os.path.join(ROOT, "src", "main", "webapp", "front", "index.html")
|
||||
|
||||
replace = {
|
||||
"WEB-INF/classes/org/apereo/cas/web/custom/controller/LoginController.class":
|
||||
os.path.join(CLASSES_DIR, "LoginController.class"),
|
||||
"WEB-INF/classes/org/apereo/cas/web/custom/controller/LoginController$LocalAuthenticationResult.class":
|
||||
os.path.join(CLASSES_DIR, "LoginController$LocalAuthenticationResult.class"),
|
||||
"front/js/conf.js": FRONT_JS,
|
||||
"front/index.html": FRONT_IDX,
|
||||
}
|
||||
|
||||
# 校验待替换源文件存在
|
||||
for k, v in replace.items():
|
||||
if not os.path.isfile(v):
|
||||
raise SystemExit("缺少源文件: %s -> %s" % (k, v))
|
||||
|
||||
with zipfile.ZipFile(BASE, "r") as z:
|
||||
names = set(z.namelist())
|
||||
# 确认条目存在(打印实际命中)
|
||||
for k in replace:
|
||||
print("命中" if k in names else "缺失(!)", k)
|
||||
for k in list(replace):
|
||||
if k not in names:
|
||||
raise SystemExit("基包中找不到条目: " + k)
|
||||
with zipfile.ZipFile(NEW, "w", zipfile.ZIP_DEFLATED) as out:
|
||||
for info in z.infolist():
|
||||
if info.filename in replace:
|
||||
data = open(replace[info.filename], "rb").read()
|
||||
ni = zipfile.ZipInfo(info.filename)
|
||||
ni.date_time = info.date_time
|
||||
ni.external_attr = info.external_attr
|
||||
ni.compress_type = zipfile.ZIP_DEFLATED
|
||||
out.writestr(ni, data)
|
||||
print("替换:", info.filename, "->", len(data), "bytes")
|
||||
else:
|
||||
out.writestr(info, z.read(info.filename))
|
||||
print("写盘:", NEW)
|
||||
|
||||
# 校验新包
|
||||
with zipfile.ZipFile(NEW, "r") as z:
|
||||
bad = z.testzip()
|
||||
if bad:
|
||||
raise SystemExit("WAR 损坏条目: " + bad)
|
||||
nl = z.namelist()
|
||||
assert "WEB-INF/lib/" in nl or any(n.startswith("WEB-INF/lib/") for n in nl), "缺少 WEB-INF/lib"
|
||||
mf = z.read("META-INF/MANIFEST.MF").decode("utf-8", "replace")
|
||||
print("MANIFEST Main-Class:", [l for l in mf.splitlines() if l.startswith("Main-Class")])
|
||||
for k in replace:
|
||||
print("存在" if k in nl else "丢失(!)", k)
|
||||
|
||||
# 原子替换
|
||||
bak = BASE + ".bak7_31"
|
||||
if not os.path.exists(bak):
|
||||
shutil.move(BASE, bak)
|
||||
shutil.move(NEW, BASE)
|
||||
print("已生成新 target/cas.war, 旧包备份为 cas.war.bak7_31")
|
||||
@@ -0,0 +1,130 @@
|
||||
#!/bin/sh
|
||||
# CAS 启动入口:
|
||||
# 1) 从 Nacos 拉取「公共基础配置」(common.properties) 与「CAS 自身配置」(cas-prod.properties)
|
||||
# 2) 用 common.properties 的 MySQL 连接 + cas-prod.properties 的 database 拼出完整 JDBC URL
|
||||
# 3) 写出 /etc/cas/config/cas.properties 后启动 CAS
|
||||
# 说明:CAS 不自建 MySQL,统一读 Nacos 公共配置;库名写在 CAS 自己的配置里。
|
||||
set -e
|
||||
|
||||
CONFIG_PATH=/etc/cas/config/cas.properties
|
||||
# 备份镜像内烤好的兜底配置,供 Nacos 不可用时回退
|
||||
cp -f "$CONFIG_PATH" "${CONFIG_PATH}.baked" 2>/dev/null || true
|
||||
|
||||
NACOS_ADDR="${NACOS_ADDR:-nacos:8848}"
|
||||
NACOS_USER="${NACOS_USER:-nacos}"
|
||||
NACOS_PASS="${NACOS_PASS:-nacos}"
|
||||
|
||||
# 公共基础配置(含 mysql / redis / rabbitmq 连接信息)
|
||||
BASE_DATA_ID="${CAS_BASE_CONFIG_DATA_ID:-common.properties}"
|
||||
BASE_GROUP="${CAS_BASE_CONFIG_GROUP:-DEFAULT_GROUP}"
|
||||
BASE_NS="${CAS_BASE_CONFIG_NAMESPACE:-public}"
|
||||
# CAS 自身配置
|
||||
CAS_DATA_ID="${CAS_CONFIG_DATA_ID:-cas-prod.properties}"
|
||||
CAS_GROUP="${CAS_CONFIG_GROUP:-CAS}"
|
||||
CAS_NS="${CAS_CONFIG_NAMESPACE:-cas}"
|
||||
|
||||
TOKEN=""
|
||||
|
||||
# 取 Nacos 登录 token(鉴权关闭时返回空,fetch 会自动降级)
|
||||
get_token() {
|
||||
TOKEN=$(curl -s -m 10 -X POST "http://${NACOS_ADDR}/nacos/v1/auth/login?username=${NACOS_USER}&password=${NACOS_PASS}" \
|
||||
| sed -n 's/.*"accessToken":"\([^"]*\)".*/\1/p')
|
||||
}
|
||||
|
||||
# 拉取单个配置到 $4;带 token 失败则自动不带 token 重试
|
||||
fetch() {
|
||||
local code
|
||||
if [ -n "$TOKEN" ]; then
|
||||
code=$(curl -s -m 10 -o "$4" -w "%{http_code}" "http://${NACOS_ADDR}/nacos/v1/cs/configs?dataId=$1&group=$2&tenant=$3&accessToken=${TOKEN}")
|
||||
fi
|
||||
if [ -z "$TOKEN" ] || [ "$code" != "200" ] || [ ! -s "$4" ]; then
|
||||
code=$(curl -s -m 10 -o "$4" -w "%{http_code}" "http://${NACOS_ADDR}/nacos/v1/cs/configs?dataId=$1&group=$2&tenant=$3")
|
||||
fi
|
||||
if [ "$code" = "200" ] && [ -s "$4" ]; then
|
||||
echo "[run-cas] 已加载 $1 (group=$2, ns=$3)"
|
||||
return 0
|
||||
fi
|
||||
echo "[run-cas] 拉取 $1 失败 http=${code}"
|
||||
return 1
|
||||
}
|
||||
|
||||
# 精确读取 key=value 中的 value(兼容 value 内含 =)
|
||||
val() {
|
||||
awk -v k="$2" 'index($0,"=")>0 && substr($0,1,index($0,"=")-1)==k {print substr($0,index($0,"=")+1); exit}' "$1"
|
||||
}
|
||||
|
||||
# 读键:优先 spring.datasource.*,回退 mysql.* 风格(兼容两种命名习惯)
|
||||
readkey() {
|
||||
local f="$1" k="$2" v
|
||||
v=$(val "$f" "$k"); [ -n "$v" ] && { echo "$v"; return; }
|
||||
v=$(val "$f" "mysql.${k#spring.datasource.}"); [ -n "$v" ] && { echo "$v"; return; }
|
||||
[ "$k" = "spring.datasource.username" ] && val "$f" "mysql.user"
|
||||
}
|
||||
|
||||
restore_baked() {
|
||||
echo "[run-cas] 回退到镜像内烤好的默认配置"
|
||||
cp -f "${CONFIG_PATH}.baked" "$CONFIG_PATH"
|
||||
exec java -jar /app/cas.war
|
||||
}
|
||||
|
||||
get_token
|
||||
|
||||
BASE_TMP="${CONFIG_PATH}.base"
|
||||
CAS_TMP="${CONFIG_PATH}.cas"
|
||||
|
||||
# 1) 公共基础配置(MySQL 连接必须在此)
|
||||
if ! fetch "$BASE_DATA_ID" "$BASE_GROUP" "$BASE_NS" "$BASE_TMP"; then
|
||||
echo "[run-cas] 公共配置缺失,无法拼 JDBC,回退兜底"
|
||||
restore_baked
|
||||
fi
|
||||
|
||||
DS_HOST=$(readkey "$BASE_TMP" "spring.datasource.host")
|
||||
DS_PORT=$(readkey "$BASE_TMP" "spring.datasource.port")
|
||||
[ -z "$DS_PORT" ] && DS_PORT=3306
|
||||
DS_USER=$(readkey "$BASE_TMP" "spring.datasource.username")
|
||||
DS_PASS=$(readkey "$BASE_TMP" "spring.datasource.password")
|
||||
DS_PARAMS=$(readkey "$BASE_TMP" "spring.datasource.params")
|
||||
[ -z "$DS_PARAMS" ] && DS_PARAMS="useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true"
|
||||
|
||||
# 占位符保护:common.properties 未替换占位符时明确报错,避免静默用假凭据导致连不上 MySQL
|
||||
case "${DS_USER}${DS_PASS}" in
|
||||
*__SHARED_*) echo "[run-cas] 致命:common.properties 中 MySQL 用户名/密码仍为占位符(__SHARED_*),请在 Nacos 填入真实 mysql8 凭据后再启动"; exit 1 ;;
|
||||
esac
|
||||
|
||||
# 2) CAS 自身配置(库名 database 在此;缺省回退 bsch_userbase)
|
||||
DB_NAME=bsch_userbase
|
||||
if fetch "$CAS_DATA_ID" "$CAS_GROUP" "$CAS_NS" "$CAS_TMP"; then
|
||||
_db=$(val "$CAS_TMP" "cas.authn.jdbc.query[0].database")
|
||||
[ -n "$_db" ] && DB_NAME="$_db"
|
||||
else
|
||||
echo "[run-cas] CAS 自身配置缺失,database 用默认 bsch_userbase"
|
||||
fi
|
||||
|
||||
if [ -z "$DS_HOST" ]; then
|
||||
echo "[run-cas] 公共配置缺少 MySQL host,无法拼 JDBC,回退兜底"
|
||||
restore_baked
|
||||
fi
|
||||
|
||||
# 3) 拼完整 JDBC URL
|
||||
JDBC_URL="jdbc:mysql://${DS_HOST}:${DS_PORT}/${DB_NAME}?${DS_PARAMS}"
|
||||
|
||||
# 4) 合并:CAS 配置为主,过滤其中旧的 JDBC 连接项,注入公共连接
|
||||
{
|
||||
if [ -f "$CAS_TMP" ]; then
|
||||
grep -vF 'cas.authn.jdbc.query[0].url=' "$CAS_TMP" \
|
||||
| grep -vF 'cas.authn.jdbc.query[0].user=' \
|
||||
| grep -vF 'cas.authn.jdbc.query[0].password=' \
|
||||
| grep -vF 'cas.authn.jdbc.query[0].driverClass='
|
||||
fi
|
||||
echo "cas.authn.jdbc.query[0].url=${JDBC_URL}"
|
||||
[ -n "$DS_USER" ] && echo "cas.authn.jdbc.query[0].user=${DS_USER}"
|
||||
[ -n "$DS_PASS" ] && echo "cas.authn.jdbc.query[0].password=${DS_PASS}"
|
||||
echo "cas.authn.jdbc.query[0].driverClass=com.mysql.jdbc.Driver"
|
||||
# 透传公共配置里的 redis / rabbitmq 连接(自定义 /api/login 的 loginService 依赖 Redis 存验证码/失败计数/账号锁定)
|
||||
grep -E '^spring\.redis\.' "$BASE_TMP" 2>/dev/null || true
|
||||
grep -E '^spring\.rabbitmq\.' "$BASE_TMP" 2>/dev/null || true
|
||||
} > "$CONFIG_PATH"
|
||||
|
||||
echo "[run-cas] 生效 JDBC URL: ${JDBC_URL}"
|
||||
echo "[run-cas] 启动 CAS ..."
|
||||
exec java -jar /app/cas.war
|
||||
@@ -0,0 +1,28 @@
|
||||
import paramiko
|
||||
|
||||
HOST = "192.168.10.157"; PORT = 22; USER = "yunhai"; PASS = "123456"
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect(HOST, port=PORT, username=USER, password=PASS, timeout=20)
|
||||
|
||||
def run(cmd):
|
||||
stdin, stdout, stderr = ssh.exec_command(cmd)
|
||||
out = stdout.read().decode('utf-8', 'ignore')
|
||||
err = stderr.read().decode('utf-8', 'ignore')
|
||||
return out + ("ERR:" + err if err else "")
|
||||
|
||||
print("=== 1) running container image & status ===")
|
||||
print(run("docker ps --filter name=cas-sso --format '{{.Image}} | {{.Status}}'"))
|
||||
print("inspect image id:")
|
||||
print(run("docker inspect cas-sso --format '{{.Image}}' 2>/dev/null"))
|
||||
|
||||
print("\n=== 2) effective cas.properties default-redirect-url (container) ===")
|
||||
print(run("docker exec cas-sso grep -ri 'default-redirect-url' /etc/cas/config/ 2>/dev/null || echo 'NOT FOUND in /etc/cas/config'"))
|
||||
print(run("docker exec cas-sso sh -c \"cat /etc/cas/config/cas.properties 2>/dev/null | grep -i redirect\" || echo none"))
|
||||
|
||||
print("\n=== 3) controller class feature in RUNNING war (v1.8 has window.location.href) ===")
|
||||
print(run("docker cp cas-sso:/app/cas.war /tmp/live_cas.war 2>&1 && echo COPIED"))
|
||||
# analyze with python on the box? simpler: grep for the literal bytecode string in the war zip entry
|
||||
print(run("docker exec cas-sso sh -c \"cd /tmp && python3 - <<'PY'\nimport zipfile\nz=zipfile.ZipFile('/tmp/live_cas.war')\nc=z.read('WEB-INF/classes/org/apereo/cas/web/custom/controller/LoginController.class').decode('latin1')\nprint('has window.location.href :', 'window.location.href' in c)\nprint('has sendRedirect :', 'sendRedirect' in c)\nprint('has text/html :', 'text/html' in c)\nPY\n\" 2>&1 || echo 'python3 not on box'"))
|
||||
|
||||
ssh.close()
|
||||
@@ -0,0 +1,36 @@
|
||||
import paramiko, zipfile
|
||||
|
||||
HOST="192.168.10.157"; USER="yunhai"; PASS="123456"
|
||||
LOCAL = "D:/SoftProject/yunhai/CasSso/cas-overlay-template-5.3/docker/live_cas.war"
|
||||
ssh=paramiko.SSHClient(); ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect(HOST, port=22, username=USER, password=PASS, timeout=20)
|
||||
|
||||
def run(c):
|
||||
i,o,e=ssh.exec_command(c)
|
||||
return o.read().decode('utf-8','ignore') + (("ERR:"+e.read().decode('utf-8','ignore')) if e.read() else "")
|
||||
|
||||
print(run("docker cp cas-sso:/app/cas.war /tmp/live_cas.war && echo COPIED_OK"))
|
||||
sftp=ssh.open_sftp()
|
||||
sftp.get('/tmp/live_cas.war', LOCAL)
|
||||
sftp.close()
|
||||
print("local war size:", __import__('os').path.getsize(LOCAL))
|
||||
|
||||
z=zipfile.ZipFile(LOCAL)
|
||||
c=z.read('WEB-INF/classes/org/apereo/cas/web/custom/controller/LoginController.class').decode('latin1')
|
||||
print("\n=== RUNNING WAR LoginController features ===")
|
||||
print("has window.location.href (v1.8 success):", 'window.location.href' in c)
|
||||
print("has sendRedirect (error path):", 'sendRedirect' in c)
|
||||
print("has text/html (v1.8 success):", 'text/html' in c)
|
||||
idx=z.read('front/index.html').decode('utf-8','replace')
|
||||
print("\n=== RUNNING WAR index.html ===")
|
||||
print("has action=/api/login:", 'action="/api/login"' in idx)
|
||||
print("has ?v=1.8:", '?v=1.8' in idx)
|
||||
print("has name=username:", 'name="username"' in idx)
|
||||
|
||||
print("\n=== container filesystem: any default-redirect-url? ===")
|
||||
print(run("grep -rni 'default-redirect-url' /etc/cas 2>/dev/null; grep -rni 'default-redirect-url' /app/*.properties 2>/dev/null; echo '---env---'; docker exec cas-sso env 2>/dev/null | grep -i 'REDIRECT\\|TGC\\|CAS_CUSTOM' "))
|
||||
print("\n=== effective cas.properties (head) ===")
|
||||
print(run("docker exec cas-sso sh -c 'cat /etc/cas/config/cas.properties 2>/dev/null | head -60'"))
|
||||
print("\n=== JDBC url in container ===")
|
||||
print(run("docker exec cas-sso sh -c 'cat /etc/cas/config/cas.properties 2>/dev/null | grep -i jdbc'"))
|
||||
ssh.close()
|
||||
@@ -0,0 +1,8 @@
|
||||
import paramiko
|
||||
HOST="192.168.10.157"; PORT=22; USER="yunhai"; PASS="123456"
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect(HOST, port=PORT, username=USER, password=PASS, timeout=20)
|
||||
stdin, stdout, stderr = ssh.exec_command("docker logs --tail 80 cas-sso 2>&1 | tail -80")
|
||||
print(stdout.read().decode('utf-8','ignore'))
|
||||
ssh.close()
|
||||
@@ -0,0 +1,30 @@
|
||||
#!/usr/bin/env python3
|
||||
# 通过 SFTP 上传文件到远端(参数: host port user local remote [local remote ...])
|
||||
# 依赖环境变量 SSHPASS
|
||||
import os, sys, paramiko
|
||||
|
||||
def main():
|
||||
host, port, user = sys.argv[1], int(sys.argv[2]) if len(sys.argv) > 2 else 22, (sys.argv[3] if len(sys.argv) > 3 else "root")
|
||||
pw = os.environ.get("SSHPASS")
|
||||
if not pw:
|
||||
sys.stderr.write("ERROR: 请先设置环境变量 SSHPASS\n"); sys.exit(1)
|
||||
t = paramiko.Transport((host, port))
|
||||
t.connect(username=user, password=pw)
|
||||
sftp = paramiko.SFTPClient.from_transport(t)
|
||||
args = sys.argv[4:]
|
||||
for i in range(0, len(args), 2):
|
||||
local, remote = args[i], args[i+1]
|
||||
parts = [p for p in os.path.dirname(remote).split("/") if p]
|
||||
cur = ""
|
||||
for p in parts:
|
||||
cur += "/" + p
|
||||
try: sftp.stat(cur)
|
||||
except IOError:
|
||||
try: sftp.mkdir(cur)
|
||||
except IOError: pass
|
||||
sftp.put(local, remote)
|
||||
print(f"PUT {local} -> {remote}")
|
||||
t.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user