#!/usr/bin/env python3 # 启动前从 Nacos 拉取 common.yaml + cas.yaml,生成 /etc/cas/config/cas.properties。 # 替代失效的 EnvironmentPostProcessor(CAS 5.3 + Spring Cloud bootstrap 下 EPP 不触发)。 # CAS 5.3 原生加载 /etc/cas/config/cas.properties(profile=standalone),优先级高于 classpath application.yml。 import os, sys, json, urllib.request, urllib.parse, yaml ADDR = os.environ.get("NACOS_ADDR", "nacos.yunhaixingtong.com:8848") NS = os.environ.get("NACOS_NAMESPACE", "dev") USER = os.environ.get("NACOS_USER", "yh-reader") PASS = os.environ.get("NACOS_PASS", "123456") OUT = os.environ.get("CAS_PROPERTIES", "/etc/cas/config/cas.properties") # 库名默认值(cas.yaml 的 cas.authn.jdbc.query[0].database 仅用于拼 jdbc url,不作为独立属性写入) DEFAULT_DB = "bsch_userbase" DEFAULT_PARAMS = "useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true" # CAS 不识别的属性(写入会导致 RelaxedBinding 崩溃),从输出中排除 EXCLUDE_KEYS = {"cas.authn.jdbc.query[0].database", "cas.authn.jdbc.query[0].dialect"} # 本地兜底配置(与运行环境无关,烤进 cas.properties) LOCAL_CONFIG = """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 logging.config: file:/etc/cas/config/log4j2.xml cas.adminPagesSecurity.ip=127\\.0\\.0\\.1 """ def http_get(url, timeout=10): try: with urllib.request.urlopen(url, timeout=timeout) as r: return r.read().decode("utf-8") if r.status == 200 else None except Exception: return None def http_post(url, data=b"", timeout=10): try: req = urllib.request.Request(url, data=data, method="POST") with urllib.request.urlopen(req, timeout=timeout) as r: return r.read().decode("utf-8") if r.status == 200 else None except Exception: return None def login(): url = "http://%s/nacos/v1/auth/login?username=%s&password=%s" % ( ADDR, urllib.parse.quote(USER), urllib.parse.quote(PASS)) resp = http_post(url) if not resp: return None try: return json.loads(resp).get("accessToken") except Exception: i = resp.find('"accessToken"') if i < 0: return None s = resp.find('"', i + 14) e = resp.find('"', s + 1) return resp[s + 1:e] if s >= 0 and e > s else None def resolve_ns(token): if not NS: return "" url = "http://%s/nacos/v1/console/namespaces" % ADDR if token: url += "?accessToken=" + urllib.parse.quote(token) body = http_get(url) if not body: return NS try: data = json.loads(body).get("data", []) except Exception: return NS for item in data: if NS == item.get("namespaceShowName") or NS == item.get("namespace"): return item.get("namespace", "") return NS def fetch_yaml(data_id, group, nsid, token): base = "http://%s/nacos/v1/cs/configs" % ADDR qs = "dataId=%s&group=%s&tenant=%s" % ( urllib.parse.quote(data_id), urllib.parse.quote(group), urllib.parse.quote(nsid)) if token: content = http_get("%s?%s&accessToken=%s" % (base, qs, urllib.parse.quote(token))) if content and content.strip() and '"error"' not in content: return content return http_get("%s?%s" % (base, qs)) or "" def flatten(prefix, value, out): if isinstance(value, dict): for k, v in value.items(): flatten(k if not prefix else prefix + "." + k, v, out) elif isinstance(value, list): for i, v in enumerate(value): flatten("%s[%d]" % (prefix, i) if prefix else "[%d]" % i, v, out) elif value is not None: out[prefix] = value def parse_yaml(content): if not content or not content.strip(): return {} data = yaml.safe_load(content) or {} flat = {} flatten("", data, flat) return flat def esc(v): if isinstance(v, bool): return "true" if v else "false" s = str(v) s = s.replace("\\", "\\\\") return s def build(common, cas): merged = {} # cas.yaml 透传(排除无效属性) for k, v in cas.items(): if k not in EXCLUDE_KEYS: merged[k] = v # datasource -> jdbc url(与 EPP buildCasProperties 逻辑一致) ds_host = common.get("spring.datasource.host") or common.get("mysql.host") ds_port = common.get("spring.datasource.port") or common.get("mysql.port") or 3306 ds_user = common.get("spring.datasource.username") or common.get("mysql.user") ds_pass = common.get("spring.datasource.password") or common.get("mysql.password") ds_params = common.get("spring.datasource.params") or common.get("mysql.params") or DEFAULT_PARAMS db_name = cas.get("cas.authn.jdbc.query[0].database", DEFAULT_DB) if ds_host: jdbc_url = "jdbc:mysql://%s:%s/%s?%s" % (ds_host, ds_port, db_name, ds_params) merged["cas.authn.jdbc.query[0].url"] = jdbc_url if ds_user: merged["cas.authn.jdbc.query[0].user"] = ds_user if ds_pass: merged["cas.authn.jdbc.query[0].password"] = ds_pass merged["cas.authn.jdbc.query[0].driverClass"] = "com.mysql.jdbc.Driver" # 透传 redis / rabbitmq for k, v in common.items(): if k.startswith("spring.redis.") or k.startswith("spring.rabbitmq."): merged[k] = v return merged def main(): token = login() nsid = resolve_ns(token) common_raw = fetch_yaml("common.yaml", "DEFAULT_GROUP", nsid, token) cas_raw = fetch_yaml("cas.yaml", "CAS", nsid, token) common = parse_yaml(common_raw) cas = parse_yaml(cas_raw) if not common and not cas: print("[nacos_config] WARN: 未拉到任何 Nacos 配置,保留现有 cas.properties", file=sys.stderr) sys.exit(1) merged = build(common, cas) lines = [ "# 由 nacos_config.py 从 Nacos 生成(勿手改)", "# namespace=%s nsid=%s common_keys=%d cas_keys=%d" % (NS, nsid, len(common), len(cas)), ] for k in sorted(merged.keys()): lines.append("%s=%s" % (k, esc(merged[k]))) lines.append("") lines.append("# === 本地兜底配置 ===") lines.append(LOCAL_CONFIG.rstrip("\n")) content = "\n".join(lines) + "\n" os.makedirs(os.path.dirname(OUT), exist_ok=True) with open(OUT, "w", encoding="utf-8") as f: f.write(content) print("[nacos_config] wrote %d props -> %s" % (len(merged), OUT)) print("[nacos_config] always-captcha=%s redis.host=%s jdbc.host=%s server.name=%s" % ( merged.get("custom.login.always-captcha"), merged.get("spring.redis.host"), common.get("spring.datasource.host"), merged.get("cas.server.name"))) if __name__ == "__main__": main()