Compare commits
2
Commits
23eb1be4ad
...
07ab248db6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
07ab248db6 | ||
|
|
4301f1db8c |
+27
-39
@@ -1,4 +1,11 @@
|
|||||||
name: deploy-cassso-service
|
name: build-push
|
||||||
|
|
||||||
|
# CAS 接入 projman 部署平台后:本 workflow 只负责 构建镜像 + 推 Nexus + 通知 projman 建档,
|
||||||
|
# 部署由运维在 projman「后端发布」tab 手动选 tag 执行(App.deployMode=manual)。
|
||||||
|
# 触发:push 到 main / 手动 Run workflow
|
||||||
|
# 依赖仓库 Secrets:NEXUS_USER / NEXUS_PASSWORD(docker-hosted 推送)/ DEPLOY_TOKEN(webhook 鉴权)/ WEBHOOK_URL
|
||||||
|
# 镜像名与 projman 派生规则一致:应用名 cassso → 镜像 cassso-service(NexusClient.imageRepo = 应用名小写 + "-service")
|
||||||
|
# 注意:nexus:8083 为 HTTP 仓库,宿主 daemon 需配置 insecure-registries 才能推送(projman 一直在拉,通常已配)
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -6,54 +13,35 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-deploy:
|
build-push:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
# docker:24 自带 docker CLI;宿主 docker.sock 默认挂入 job 容器,
|
# docker:24 自带 docker CLI;宿主 docker.sock 默认挂入 job 容器
|
||||||
# 因此 docker build/run 实际都在宿主守护进程上执行。
|
|
||||||
container:
|
container:
|
||||||
image: docker:24
|
image: docker:24
|
||||||
steps:
|
steps:
|
||||||
- name: Install git (alpine)
|
- name: Install git / curl (alpine)
|
||||||
run: apk add --no-cache git
|
run: apk add --no-cache git curl
|
||||||
|
|
||||||
- name: Checkout (manual, internal Gitea)
|
- name: Checkout (manual, internal Gitea)
|
||||||
|
# cas.war (约 170MB) 已作为普通 blob 入库(服务端未启用 LFS)
|
||||||
run: |
|
run: |
|
||||||
git clone --depth 1 --branch main http://192.168.10.160:3000/${GITHUB_REPOSITORY}.git /tmp/src
|
git clone --depth 1 --branch main http://192.168.10.160:3000/${GITHUB_REPOSITORY}.git /tmp/src
|
||||||
|
COMMIT_MSG=$(git -C /tmp/src log -1 --pretty=%s | tr '\n' ' ' | sed 's/["\\]/ /g' | cut -c1-200)
|
||||||
|
echo "COMMIT_MSG=${COMMIT_MSG}" >> "$GITHUB_ENV"
|
||||||
cp -a /tmp/src/. .
|
cp -a /tmp/src/. .
|
||||||
rm -rf .git .gitea
|
rm -rf .git .gitea /tmp/src
|
||||||
rm -rf /tmp/src
|
|
||||||
|
|
||||||
# 用宿主 docker 守护进程跑 maven 3.9(--network=host 走宿主网络拉 nexus 依赖)。
|
- name: Build image (copies docker/cas.war, host daemon)
|
||||||
# 重要:job 容器内 $PWD 不是宿主可见路径,不能直接 `-v $PWD:/build` 挂给 maven 容器
|
run: docker build -f docker/Dockerfile -t nexus:8083/docker-hosted/cassso-service:${GITHUB_SHA:0:8} .
|
||||||
# (docker.sock 跨边界 bind mount 会落到宿主空目录,maven 看不到 pom.xml)。
|
|
||||||
# 改用 docker create + docker cp 把源码送进 maven 容器,构建完再把 target 拷回工作区。
|
- name: Push Nexus docker-hosted
|
||||||
# war 由 maven 从源码构建(target/cas.war),不再把 war 提交进 git。
|
|
||||||
- name: Build war (maven 3.9, nexus maven-public)
|
|
||||||
run: |
|
run: |
|
||||||
MVN=$(docker create --network=host -v /opt/docker/ubs-m2:/root/.m2 -w /build maven:3.9-eclipse-temurin-8 mvn -s maven-settings.xml -B -DskipTests package)
|
echo "${{ secrets.NEXUS_PASSWORD }}" | docker login nexus:8083 -u "${{ secrets.NEXUS_USER }}" --password-stdin
|
||||||
docker cp "$PWD/." "$MVN:/build"
|
docker push nexus:8083/docker-hosted/cassso-service:${GITHUB_SHA:0:8}
|
||||||
docker start -a "$MVN"
|
|
||||||
docker cp "$MVN:/build/target" "$PWD/target"
|
|
||||||
docker rm -f "$MVN"
|
|
||||||
|
|
||||||
- name: Build image on host daemon
|
- name: Notify projman (release pending, manual deploy)
|
||||||
# 构建上下文=仓库根目录,Dockerfile 内 COPY target/cas.war;
|
|
||||||
# 业务配置由 run-cas.sh 在启动时调用 nacos_config.py 从 Nacos 拉取(替代失效的 EPP)。
|
|
||||||
run: docker build -f docker/Dockerfile -t cassso-service:latest .
|
|
||||||
|
|
||||||
- name: Deploy on host (infra-infra-net, CAS 端口 8443)
|
|
||||||
# 镜像已在宿主本地,直接运行,无需推送到 nexus(nexus docker 仓库为 HTTP,需 daemon insecure-registries 才能推送)
|
|
||||||
# Nacos 环境变量:run-cas.sh 据此在启动阶段从 Nacos 拉取业务配置(common.yaml + cas.yaml, prod)。
|
|
||||||
# 不传 NACOS_NAMESPACE 会默认连 dev(dev 命名空间无配置),登录必失败——必须显式传 prod。
|
|
||||||
# 注意:镜像构建成功才会走到本步;若上面 maven/docker build 失败,本步不执行,旧容器继续运行。
|
|
||||||
run: |
|
run: |
|
||||||
docker rm -f cas-sso 2>/dev/null || true
|
curl -sS -X POST "${{ secrets.WEBHOOK_URL }}" \
|
||||||
docker run -d --name cas-sso --network infra-infra-net -p 8443:8443 --restart unless-stopped \
|
-H "Content-Type: application/json" \
|
||||||
-e NACOS_ADDR=nacos:8848 \
|
-H "X-Deploy-Token: ${{ secrets.DEPLOY_TOKEN }}" \
|
||||||
-e NACOS_NAMESPACE=prod \
|
-d "{\"app\":\"cassso\",\"tag\":\"${GITHUB_SHA:0:8}\",\"sha\":\"${GITHUB_SHA}\",\"message\":\"${COMMIT_MSG}\"}"
|
||||||
-e NACOS_USER=yh-reader \
|
|
||||||
-e NACOS_PASS=123456 \
|
|
||||||
cassso-service:latest
|
|
||||||
|
|
||||||
- name: Done
|
|
||||||
run: 'echo "CasSsoService deployed -> http://sso.yunhaixingtong.com/cas"'
|
|
||||||
|
|||||||
+6
-9
@@ -1,4 +1,4 @@
|
|||||||
# CAS 5.3.16 镜像(cas.war 由 CI 用 maven 从源码构建,落在 target/cas.war)
|
# CAS 5.3.16 镜像(基于已构建好的 target/cas.war)
|
||||||
# 构建上下文 = 项目根目录: docker build -f docker/Dockerfile -t <img> .
|
# 构建上下文 = 项目根目录: docker build -f docker/Dockerfile -t <img> .
|
||||||
FROM eclipse-temurin:8-jre
|
FROM eclipse-temurin:8-jre
|
||||||
|
|
||||||
@@ -7,21 +7,18 @@ RUN mkdir -p /etc/cas/config \
|
|||||||
&& keytool -genkeypair -alias cas -keyalg RSA -keysize 2048 -validity 3650 \
|
&& keytool -genkeypair -alias cas -keyalg RSA -keysize 2048 -validity 3650 \
|
||||||
-storetype JKS -keystore /etc/cas/cas.keystore \
|
-storetype JKS -keystore /etc/cas/cas.keystore \
|
||||||
-storepass changeit -keypass changeit -dname "CN=cas.example.org" 2>/dev/null || true \
|
-storepass changeit -keypass changeit -dname "CN=cas.example.org" 2>/dev/null || true \
|
||||||
&& apt-get update && apt-get install -y --no-install-recommends curl python3 python3-yaml \
|
&& apt-get update && apt-get install -y --no-install-recommends curl \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
# cas.war 由 maven 源码构建产出(target/cas.war),不再把 war 提交进 git
|
COPY docker/cas.war /app/cas.war
|
||||||
COPY target/cas.war /app/cas.war
|
|
||||||
COPY docker/cas.properties /etc/cas/config/cas.properties
|
COPY docker/cas.properties /etc/cas/config/cas.properties
|
||||||
COPY docker/nacos_config.py /app/nacos_config.py
|
|
||||||
COPY docker/run-cas.sh /app/run-cas.sh
|
COPY docker/run-cas.sh /app/run-cas.sh
|
||||||
COPY etc/cas/config/log4j2.xml /etc/cas/config/log4j2.xml
|
COPY etc/cas/config/log4j2.xml /etc/cas/config/log4j2.xml
|
||||||
RUN chmod +x /app/run-cas.sh
|
RUN chmod +x /app/run-cas.sh
|
||||||
|
|
||||||
# 业务配置(cas.server.name / JDBC / Redis / always-captcha 等)由 run-cas.sh 在启动阶段
|
# 镜像内 cas.properties 仅保留环境无关配置(HTTPS/端口/日志/管理页白名单);
|
||||||
# 调用 nacos_config.py 从 Nacos 原生拉取(common.yaml + cas.yaml,namespace=NACOS_NAMESPACE),
|
# 业务配置(cas.server.name / cas.authn.jdbc / Redis / custom.login.* 等)来自 projman
|
||||||
# 生成 /etc/cas/config/cas.properties(CAS 5.3 原生加载,优先级高于 classpath application.yml)。
|
# 配置管理挂载的 /app/config/application.yml(Boot 1.5 自动加载 file:./config/ 且优先于 classpath),镜像保持环境无关。
|
||||||
# 替代失效的 EnvironmentPostProcessor(CAS 5.3 + Spring Cloud bootstrap 下 EPP 不触发)。
|
|
||||||
EXPOSE 8443
|
EXPOSE 8443
|
||||||
ENTRYPOINT ["/app/run-cas.sh"]
|
ENTRYPOINT ["/app/run-cas.sh"]
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
# CAS 烤进镜像的本地兜底配置(与运行环境无关、不含任何凭据)
|
# CAS 烤进镜像的本地兜底配置(与运行环境无关、不含任何凭据)
|
||||||
# 业务配置由 Nacos 原生注入(NacosEnvironmentPostProcessor):
|
# 业务配置来自 projman 配置管理挂载的 /app/config/application.yml:
|
||||||
# common.yaml(DEFAULT_GROUP) + cas.yaml(CAS),namespace=NACOS_NAMESPACE
|
# -> cas.server.name / cas.authn.jdbc / Redis / custom.login.* 等全部由挂载文件提供,本文件不写。
|
||||||
# -> cas.server.name / JDBC 连接 / Redis 等全部来自 Nacos,本文件不写。
|
|
||||||
# 仅保留本地必需的:HTTPS 密钥库、端口、日志、管理页白名单。
|
# 仅保留本地必需的:HTTPS 密钥库、端口、日志、管理页白名单。
|
||||||
server.port: 8443
|
server.port: 8443
|
||||||
server.ssl.enabled: true
|
server.ssl.enabled: true
|
||||||
|
|||||||
Binary file not shown.
@@ -1,3 +1,8 @@
|
|||||||
|
# ⚠️ 已废弃(2026-08-26):CAS 已接入 projman 部署平台,容器编排由 projman 生成
|
||||||
|
# (容器名 cassso-service,/data/configs/cassso/docker-compose.yml),不再使用本文件。
|
||||||
|
# Nacos 配置注入已移除(NacosEnvironmentPostProcessor + spring.factories 已删除),
|
||||||
|
# 业务配置由 projman 配置管理挂载 /app/config/application.yml 提供。本文件仅保留作回滚参考。
|
||||||
|
|
||||||
# CAS 服务端编排:仅 cas 应用。
|
# CAS 服务端编排:仅 cas 应用。
|
||||||
# MySQL / Redis / RabbitMQ 均为 infra 公共实例(mysql8 等),不在本编排内自建。
|
# MySQL / Redis / RabbitMQ 均为 infra 公共实例(mysql8 等),不在本编排内自建。
|
||||||
# CAS 启动后由 NacosEnvironmentPostProcessor 从 Nacos 原生拉取:
|
# CAS 启动后由 NacosEnvironmentPostProcessor 从 Nacos 原生拉取:
|
||||||
|
|||||||
+6
-8
@@ -1,12 +1,10 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# CAS 启动入口
|
# CAS 启动入口(接入 projman 部署平台版)
|
||||||
# 配置注入:启动前用 python3 从 Nacos 拉 common.yaml+cas.yaml 生成 /etc/cas/config/cas.properties。
|
# 已移除 Nacos 配置注入:NacosEnvironmentPostProcessor + spring.factories 已删除(2026-08-26),
|
||||||
# 替代失效的 EnvironmentPostProcessor(CAS 5.3 + Spring Cloud bootstrap 下 EPP 不触发)。
|
# 容器启动不再访问 Nacos。业务配置来源(优先级高→低):
|
||||||
# CAS 5.3 原生加载 /etc/cas/config/cas.properties(profile=standalone),优先级高于 classpath application.yml。
|
# - projman 配置管理挂载的 /app/config/application.yml(容器 WORKDIR /app 下 ./config/,Spring Boot 默认搜索路径)
|
||||||
|
# - /etc/cas/config/cas.properties(镜像内置:仅 TLS/端口/日志,环境无关)
|
||||||
|
# - WAR classpath application.yml(TGC/webflow 密钥等兜底默认值)
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
echo "[run-cas] Nacos 配置注入:NACOS_ADDR=${NACOS_ADDR:-<unset>} NACOS_NAMESPACE=${NACOS_NAMESPACE:-<unset>} NACOS_USER=${NACOS_USER:-<unset>}"
|
|
||||||
|
|
||||||
python3 /app/nacos_config.py || echo "[run-cas] WARN: nacos_config.py 失败,用镜像内 cas.properties 兜底"
|
|
||||||
|
|
||||||
exec java -jar /app/cas.war
|
exec java -jar /app/cas.war
|
||||||
|
|||||||
-406
@@ -1,406 +0,0 @@
|
|||||||
package org.apereo.cas.web.custom.config.nacos;
|
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.springframework.boot.SpringApplication;
|
|
||||||
import org.springframework.boot.env.EnvironmentPostProcessor;
|
|
||||||
import org.springframework.core.Ordered;
|
|
||||||
import org.springframework.core.env.ConfigurableEnvironment;
|
|
||||||
import org.springframework.core.env.MapPropertySource;
|
|
||||||
import org.springframework.core.env.MutablePropertySources;
|
|
||||||
import org.springframework.core.env.PropertySource;
|
|
||||||
|
|
||||||
import org.yaml.snakeyaml.Yaml;
|
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
|
||||||
import java.io.InputStreamReader;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.net.HttpURLConnection;
|
|
||||||
import java.net.URL;
|
|
||||||
import java.net.URLEncoder;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 在 Spring Boot 环境准备阶段从 Nacos 拉取配置并注入 Environment,
|
|
||||||
* 使 CAS 5.3 能像 Spring Cloud 应用一样读到 Nacos 上的 common.yaml / cas.yaml。
|
|
||||||
*
|
|
||||||
* 背景:CAS 5.3 无原生 nacos config 模块,且不自带 nacos-client(其 Jackson 2.8/2.9 与
|
|
||||||
* nacos-client 期望的版本冲突)。这里直接用 Nacos 开放 HTTP API(与 run-cas.sh 同一套接口)
|
|
||||||
* 拉取配置,用 SnakeYAML(CAS webapp 已内置)解析,零额外依赖、无版本冲突风险。
|
|
||||||
*
|
|
||||||
* 注入优先级:插在 classpath application.yml 之前,因此 Nacos 配置会覆盖 application.yml 中的
|
|
||||||
* 同名键;但不覆盖系统属性/环境变量(更安全的兜底顺序)。
|
|
||||||
* 若 Nacos 不可达或配置缺失,fail-fast=false,直接跳过 -> 回退 application.yml(本地离线开发友好)。
|
|
||||||
*
|
|
||||||
* 环境隔离沿用 namespace 模型:NACOS_NAMESPACE 环境变量决定连哪个命名空间(dev/prod/空=public),
|
|
||||||
* 与 UserBase 共用同一套隔离逻辑。
|
|
||||||
*/
|
|
||||||
public class NacosEnvironmentPostProcessor implements EnvironmentPostProcessor, Ordered {
|
|
||||||
|
|
||||||
private static final String PROPERTY_SOURCE_NAME = "nacosConfig";
|
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(NacosEnvironmentPostProcessor.class);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
|
|
||||||
// 总开关:默认开启;设为 false 可完全跳过(纯离线本地开发)
|
|
||||||
if (!Boolean.parseBoolean(firstNonNull(environment.getProperty("nacos.config.enabled"), "true"))) {
|
|
||||||
log.info("[nacos] nacos.config.enabled=false,跳过 Nacos 配置加载");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 地址 / 命名空间:优先读字面环境变量 NACOS_ADDR / NACOS_NAMESPACE(与 UserBase bootstrap、
|
|
||||||
// run-cas.sh 同一套约定),再回退到 Spring Cloud 风格键,最后给默认值。
|
|
||||||
// 本地默认连 dev 命名空间(零配置),生产显式传 NACOS_NAMESPACE=prod。
|
|
||||||
String addr = firstNonNull(
|
|
||||||
environment.getProperty("NACOS_ADDR"),
|
|
||||||
environment.getProperty("spring.cloud.nacos.config.server-addr"),
|
|
||||||
environment.getProperty("nacos.config.addr"),
|
|
||||||
"nacos.yunhaixingtong.com:8848");
|
|
||||||
String namespace = firstNonNull(
|
|
||||||
environment.getProperty("NACOS_NAMESPACE"),
|
|
||||||
environment.getProperty("spring.cloud.nacos.config.namespace"),
|
|
||||||
environment.getProperty("nacos.config.namespace"),
|
|
||||||
"dev");
|
|
||||||
String username = firstNonNull(
|
|
||||||
environment.getProperty("NACOS_USER"),
|
|
||||||
environment.getProperty("NACOS_USERNAME"),
|
|
||||||
environment.getProperty("spring.cloud.nacos.config.username"),
|
|
||||||
environment.getProperty("nacos.config.username"),
|
|
||||||
"yh-reader");
|
|
||||||
String password = firstNonNull(
|
|
||||||
environment.getProperty("NACOS_PASS"),
|
|
||||||
environment.getProperty("NACOS_PASSWORD"),
|
|
||||||
environment.getProperty("spring.cloud.nacos.config.password"),
|
|
||||||
environment.getProperty("nacos.config.password"),
|
|
||||||
"123456");
|
|
||||||
|
|
||||||
String commonDataId = firstNonNull(environment.getProperty("nacos.config.common-data-id"), "common.yaml");
|
|
||||||
String commonGroup = firstNonNull(environment.getProperty("nacos.config.common-group"), "DEFAULT_GROUP");
|
|
||||||
String casDataId = firstNonNull(environment.getProperty("nacos.config.cas-data-id"), "cas.yaml");
|
|
||||||
String casGroup = firstNonNull(environment.getProperty("nacos.config.cas-group"), "CAS");
|
|
||||||
|
|
||||||
System.out.println("[EPP-DEBUG] ===== postProcessEnvironment START =====");
|
|
||||||
System.out.println("[EPP-DEBUG] NACOS_ADDR=" + addr + " NACOS_NAMESPACE=" + namespace
|
|
||||||
+ " NACOS_USER=" + username + " commonDataId=" + commonDataId + "/" + commonGroup
|
|
||||||
+ " casDataId=" + casDataId + "/" + casGroup);
|
|
||||||
try {
|
|
||||||
String token = login(addr, username, password);
|
|
||||||
System.out.println("[EPP-DEBUG] login token=" + (token == null ? "null" : token.substring(0, Math.min(20, token.length())) + "..."));
|
|
||||||
// 把命名空间名字(dev/prod)解析成 Nacos 的 namespaceId:
|
|
||||||
// 配置接口 tenant 参数只认 ID 不认名字,直接传 "prod" 会查不到配置。
|
|
||||||
String resolvedNs = resolveNamespace(addr, namespace, token);
|
|
||||||
System.out.println("[EPP-DEBUG] resolvedNs=" + resolvedNs);
|
|
||||||
|
|
||||||
Map<String, Object> common = fetchYaml(addr, commonDataId, commonGroup, resolvedNs, token);
|
|
||||||
Map<String, Object> cas = fetchYaml(addr, casDataId, casGroup, resolvedNs, token);
|
|
||||||
System.out.println("[EPP-DEBUG] common keys=" + (common == null ? 0 : common.size())
|
|
||||||
+ " cas keys=" + (cas == null ? 0 : cas.size()));
|
|
||||||
if (cas != null) {
|
|
||||||
System.out.println("[EPP-DEBUG] cas always-captcha=" + cas.get("custom.login.always-captcha")
|
|
||||||
+ " server.name=" + cas.get("cas.server.name"));
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, Object> merged = buildCasProperties(common, cas);
|
|
||||||
System.out.println("[EPP-DEBUG] merged size=" + merged.size()
|
|
||||||
+ " always-captcha=" + merged.get("custom.login.always-captcha")
|
|
||||||
+ " redis.host=" + merged.get("spring.redis.host"));
|
|
||||||
if (merged.isEmpty()) {
|
|
||||||
System.out.println("[EPP-DEBUG] merged EMPTY, fallback application.yml");
|
|
||||||
log.warn("[nacos] 未从 Nacos 解析出任何有效配置,回退 application.yml");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
MapPropertySource source = new MapPropertySource(PROPERTY_SOURCE_NAME, merged);
|
|
||||||
MutablePropertySources sources = environment.getPropertySources();
|
|
||||||
// 插在 application.yml 之前,使其覆盖 application.yml,但低于系统属性/环境变量
|
|
||||||
String appName = findApplicationConfigName(sources);
|
|
||||||
if (appName != null) {
|
|
||||||
sources.addBefore(appName, source);
|
|
||||||
} else {
|
|
||||||
sources.addFirst(source);
|
|
||||||
}
|
|
||||||
System.out.println("[EPP-DEBUG] INJECTED OK before=" + appName + " size=" + merged.size());
|
|
||||||
log.info("[nacos] 已从 Nacos 注入 {} 个配置项 (namespace='{}' -> '{}', addr={})", merged.size(), namespace, resolvedNs, addr);
|
|
||||||
} catch (Exception e) {
|
|
||||||
System.out.println("[EPP-DEBUG] FAILED: " + e);
|
|
||||||
e.printStackTrace();
|
|
||||||
log.warn("[nacos] 加载 Nacos 配置失败,回退 application.yml:{}", e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从 common.yaml + cas.yaml 构造最终生效的 CAS 属性。
|
|
||||||
* 关键点:common.yaml 是跨服务共享的「Spring Boot 标准」配置(spring.datasource.* / spring.redis.*),
|
|
||||||
* 而 CAS 的 JDBC 认证只认 cas.authn.jdbc.query[0].*,因此这里把 spring.datasource.* 翻译成
|
|
||||||
* cas.authn.jdbc.query[0].url,逻辑与 run-cas.sh 一致。
|
|
||||||
*/
|
|
||||||
private Map<String, Object> buildCasProperties(Map<String, Object> common, Map<String, Object> cas) {
|
|
||||||
Map<String, Object> out = new LinkedHashMap<>();
|
|
||||||
|
|
||||||
// 1) cas.yaml 透传(cas.* 自有配置先放入,后续 JDBC 连接键再覆盖)
|
|
||||||
if (cas != null) {
|
|
||||||
out.putAll(cas);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2) 从 common.yaml 抽取数据源(兼容 spring.datasource.* 与 mysql.* 两种命名)
|
|
||||||
String dsHost = firstNonNull(str(common, "spring.datasource.host"), str(common, "mysql.host"));
|
|
||||||
String dsPort = firstNonNull(str(common, "spring.datasource.port"), str(common, "mysql.port"), "3306");
|
|
||||||
String dsUser = firstNonNull(str(common, "spring.datasource.username"), str(common, "mysql.user"));
|
|
||||||
String dsPass = firstNonNull(str(common, "spring.datasource.password"), str(common, "mysql.password"));
|
|
||||||
String dsParams = firstNonNull(
|
|
||||||
str(common, "spring.datasource.params"),
|
|
||||||
str(common, "mysql.params"),
|
|
||||||
"useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true");
|
|
||||||
|
|
||||||
// 3) 库名来自 cas.yaml(cas.authn.jdbc.query[0].database),缺省 bsch_userbase
|
|
||||||
String dbName = firstNonNull(str(cas, "cas.authn.jdbc.query[0].database"), "bsch_userbase");
|
|
||||||
|
|
||||||
// 4) 拼 JDBC URL 并覆盖连接键(与 run-cas.sh 逻辑一致)
|
|
||||||
if (dsHost != null && !dsHost.isEmpty()) {
|
|
||||||
String jdbcUrl = "jdbc:mysql://" + dsHost + ":" + dsPort + "/" + dbName + "?" + dsParams;
|
|
||||||
out.put("cas.authn.jdbc.query[0].url", jdbcUrl);
|
|
||||||
if (dsUser != null) {
|
|
||||||
out.put("cas.authn.jdbc.query[0].user", dsUser);
|
|
||||||
}
|
|
||||||
if (dsPass != null) {
|
|
||||||
out.put("cas.authn.jdbc.query[0].password", dsPass);
|
|
||||||
}
|
|
||||||
out.put("cas.authn.jdbc.query[0].driverClass", "com.mysql.jdbc.Driver");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5) 透传 common.yaml 里的 redis / rabbitmq 连接(自定义 /api/login 依赖 Redis 存验证码/失败计数)
|
|
||||||
if (common != null) {
|
|
||||||
for (Map.Entry<String, Object> e : common.entrySet()) {
|
|
||||||
String k = e.getKey();
|
|
||||||
if (k.startsWith("spring.redis.") || k.startsWith("spring.rabbitmq.")) {
|
|
||||||
out.put(k, e.getValue());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String findApplicationConfigName(MutablePropertySources sources) {
|
|
||||||
for (PropertySource<?> ps : sources) {
|
|
||||||
if (ps.getName() != null && ps.getName().contains("applicationConfig")) {
|
|
||||||
return ps.getName();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 把命名空间名字(dev/prod/public)解析成 Nacos 的 namespaceId。
|
|
||||||
* Nacos 配置接口 tenant 参数只认 namespaceId,不认命名空间显示名,因此必须转换。
|
|
||||||
* 若无法解析(API 不可达/无权限),原样返回输入,调用方把它当作 ID 兜底。
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
private String resolveNamespace(String addr, String namespace, String token) {
|
|
||||||
if (namespace == null || namespace.isEmpty()) {
|
|
||||||
return namespace; // public 的 namespaceId 即空串
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
String url = "http://" + addr + "/nacos/v1/console/namespaces";
|
|
||||||
if (token != null && !token.isEmpty()) {
|
|
||||||
url += "?accessToken=" + enc(token);
|
|
||||||
}
|
|
||||||
String body = httpGet(url);
|
|
||||||
if (body == null || body.trim().isEmpty()) {
|
|
||||||
return namespace;
|
|
||||||
}
|
|
||||||
// Nacos 返回的是 JSON,而 JSON 是 YAML 子集,可直接用内置 SnakeYAML 解析(零额外依赖)
|
|
||||||
Yaml yaml = new Yaml();
|
|
||||||
Object loaded = yaml.load(body);
|
|
||||||
if (!(loaded instanceof Map)) {
|
|
||||||
return namespace;
|
|
||||||
}
|
|
||||||
Object data = ((Map<String, Object>) loaded).get("data");
|
|
||||||
if (!(data instanceof List)) {
|
|
||||||
return namespace;
|
|
||||||
}
|
|
||||||
for (Object item : (List<Object>) data) {
|
|
||||||
if (!(item instanceof Map)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
Map<String, Object> m = (Map<String, Object>) item;
|
|
||||||
String id = str(m, "namespace");
|
|
||||||
String showName = str(m, "namespaceShowName");
|
|
||||||
if (namespace.equals(showName) || namespace.equals(id)) {
|
|
||||||
log.info("[nacos] 命名空间 '{}' 解析为 namespaceId='{}'", namespace, id);
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.warn("[nacos] 解析命名空间失败,按原值 '{}' 作为 namespaceId 兜底:{}", namespace, e.getMessage());
|
|
||||||
}
|
|
||||||
return namespace;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 拉取并解析单个 Nacos 配置(YAML),失败返回 null */
|
|
||||||
private Map<String, Object> fetchYaml(String addr, String dataId, String group, String namespace, String token) {
|
|
||||||
String content = getConfig(addr, dataId, group, namespace, token);
|
|
||||||
if (content == null || content.trim().isEmpty()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
Yaml yaml = new Yaml();
|
|
||||||
Object loaded = yaml.load(content);
|
|
||||||
Map<String, Object> flat = new LinkedHashMap<>();
|
|
||||||
flatten("", loaded, flat);
|
|
||||||
return flat;
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.warn("[nacos] 解析 {} 失败:{}", dataId, e.getMessage());
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 递归展平为点号键(列表用 [i] 下标,与 CAS RelaxedBinding 兼容,如 cas.authn.jdbc.query[0].url) */
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
private void flatten(String prefix, Object value, Map<String, Object> out) {
|
|
||||||
if (value instanceof Map) {
|
|
||||||
for (Map.Entry<String, Object> e : ((Map<String, Object>) value).entrySet()) {
|
|
||||||
String key = String.valueOf(e.getKey());
|
|
||||||
String np = prefix.isEmpty() ? key : prefix + "." + key;
|
|
||||||
flatten(np, e.getValue(), out);
|
|
||||||
}
|
|
||||||
} else if (value instanceof List) {
|
|
||||||
List<Object> list = (List<Object>) value;
|
|
||||||
for (int i = 0; i < list.size(); i++) {
|
|
||||||
flatten(prefix + "[" + i + "]", list.get(i), out);
|
|
||||||
}
|
|
||||||
} else if (value != null) {
|
|
||||||
out.put(prefix, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** HTTP GET 拉取 Nacos 配置;鉴权开启时带 token,失败自动不带 token 重试 */
|
|
||||||
private String getConfig(String addr, String dataId, String group, String namespace, String token) {
|
|
||||||
String base = "http://" + addr + "/nacos/v1/cs/configs";
|
|
||||||
String qs = "dataId=" + enc(dataId) + "&group=" + enc(group) + "&tenant=" + enc(namespace);
|
|
||||||
if (token != null && !token.isEmpty()) {
|
|
||||||
String body = httpGet(base + "?" + qs + "&accessToken=" + enc(token));
|
|
||||||
if (body != null && !body.trim().isEmpty() && !body.contains("\"error\"")) {
|
|
||||||
return body;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return httpGet(base + "?" + qs);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 登录 Nacos 换取 accessToken;鉴权关闭时返回 null(调用方自动降级) */
|
|
||||||
private String login(String addr, String username, String password) {
|
|
||||||
try {
|
|
||||||
String url = "http://" + addr + "/nacos/v1/auth/login?username=" + enc(username) + "&password=" + enc(password);
|
|
||||||
String resp = httpPost(url, "");
|
|
||||||
if (resp == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
int i = resp.indexOf("\"accessToken\"");
|
|
||||||
if (i >= 0) {
|
|
||||||
int s = resp.indexOf('"', i + 14);
|
|
||||||
int e = resp.indexOf('"', s + 1);
|
|
||||||
if (s >= 0 && e > s) {
|
|
||||||
return resp.substring(s + 1, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.warn("[nacos] 登录 Nacos 失败(可能是鉴权关闭):{}", e.getMessage());
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String httpGet(String urlStr) {
|
|
||||||
HttpURLConnection conn = null;
|
|
||||||
try {
|
|
||||||
URL url = new URL(urlStr);
|
|
||||||
conn = (HttpURLConnection) url.openConnection();
|
|
||||||
conn.setRequestMethod("GET");
|
|
||||||
conn.setConnectTimeout(10000);
|
|
||||||
conn.setReadTimeout(10000);
|
|
||||||
if (conn.getResponseCode() == 200) {
|
|
||||||
return readBody(conn);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.warn("[nacos] GET {} 失败:{}", urlStr, e.getMessage());
|
|
||||||
} finally {
|
|
||||||
if (conn != null) {
|
|
||||||
conn.disconnect();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String httpPost(String urlStr, String body) {
|
|
||||||
HttpURLConnection conn = null;
|
|
||||||
try {
|
|
||||||
URL url = new URL(urlStr);
|
|
||||||
conn = (HttpURLConnection) url.openConnection();
|
|
||||||
conn.setRequestMethod("POST");
|
|
||||||
conn.setConnectTimeout(10000);
|
|
||||||
conn.setReadTimeout(10000);
|
|
||||||
conn.setDoOutput(true);
|
|
||||||
if (body != null && !body.isEmpty()) {
|
|
||||||
try (OutputStream os = conn.getOutputStream()) {
|
|
||||||
os.write(body.getBytes(StandardCharsets.UTF_8));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (conn.getResponseCode() == 200) {
|
|
||||||
return readBody(conn);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.warn("[nacos] POST {} 失败:{}", urlStr, e.getMessage());
|
|
||||||
} finally {
|
|
||||||
if (conn != null) {
|
|
||||||
conn.disconnect();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String readBody(HttpURLConnection conn) throws Exception {
|
|
||||||
try (BufferedReader br = new BufferedReader(
|
|
||||||
new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) {
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
String line;
|
|
||||||
while ((line = br.readLine()) != null) {
|
|
||||||
sb.append(line);
|
|
||||||
}
|
|
||||||
return sb.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String enc(String s) {
|
|
||||||
try {
|
|
||||||
return URLEncoder.encode(s == null ? "" : s, "UTF-8");
|
|
||||||
} catch (Exception e) {
|
|
||||||
return s == null ? "" : s;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String firstNonNull(String... vals) {
|
|
||||||
for (String v : vals) {
|
|
||||||
if (v != null && !v.isEmpty()) {
|
|
||||||
return v;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String str(Map<String, Object> m, String key) {
|
|
||||||
if (m == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
Object v = m.get(key);
|
|
||||||
return v == null ? null : String.valueOf(v);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getOrder() {
|
|
||||||
// 在 ConfigFileApplicationListener(HIGHEST_PRECEDENCE+10,负责加载 application.yml)之后执行,
|
|
||||||
// 确保 application.yml 已加载,从而能把它作为插入锚点并覆盖它
|
|
||||||
return Ordered.LOWEST_PRECEDENCE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -18,6 +18,7 @@ import org.apereo.cas.authentication.principal.Principal;
|
|||||||
import org.apereo.cas.authentication.principal.Service;
|
import org.apereo.cas.authentication.principal.Service;
|
||||||
import org.apereo.cas.authentication.principal.DefaultPrincipalFactory;
|
import org.apereo.cas.authentication.principal.DefaultPrincipalFactory;
|
||||||
import org.apereo.cas.authentication.principal.WebApplicationServiceFactory;
|
import org.apereo.cas.authentication.principal.WebApplicationServiceFactory;
|
||||||
|
import org.apereo.cas.services.UnauthorizedServiceException;
|
||||||
import org.apereo.cas.ticket.AbstractTicketException;
|
import org.apereo.cas.ticket.AbstractTicketException;
|
||||||
import org.apereo.cas.ticket.ServiceTicket;
|
import org.apereo.cas.ticket.ServiceTicket;
|
||||||
import org.apereo.cas.ticket.TicketGrantingTicket;
|
import org.apereo.cas.ticket.TicketGrantingTicket;
|
||||||
@@ -240,6 +241,83 @@ public class LoginController {
|
|||||||
return buildResponse(0, data, "");
|
return buildResponse(0, data, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SSO 预检:登录页加载时调用,判断当前浏览器是否已持有有效 TGT。
|
||||||
|
* 已登录且有 service → 直接签发 ST 返回业务系统回跳地址(单点免密跳转的关键);
|
||||||
|
* 已登录无 service → 返回默认落点;未登录/票据失效 → loggedIn=false,前端正常展示表单。
|
||||||
|
* 任何内部异常都按未登录降级,绝不向上抛(预检失败不应阻断正常登录流程)。
|
||||||
|
*/
|
||||||
|
@GetMapping("/check-sso")
|
||||||
|
public JSONObject checkSso(
|
||||||
|
@RequestParam(value = "service", required = false) String service,
|
||||||
|
HttpServletRequest request) {
|
||||||
|
JSONObject data = new JSONObject();
|
||||||
|
boolean loggedIn = false;
|
||||||
|
String url = "";
|
||||||
|
try {
|
||||||
|
String tgtId = ticketGrantingTicketCookieGenerator.retrieveCookieValue(request);
|
||||||
|
if (tgtId != null && !tgtId.trim().isEmpty()) {
|
||||||
|
TicketGrantingTicket tgt = null;
|
||||||
|
try {
|
||||||
|
tgt = centralAuthenticationService.getTicket(tgtId, TicketGrantingTicket.class);
|
||||||
|
} catch (AbstractTicketException e) {
|
||||||
|
// 票据不存在/已过期:按未登录处理
|
||||||
|
logger.info("check-sso:TGT {} 不存在或已过期", tgtId);
|
||||||
|
}
|
||||||
|
if (tgt != null && !tgt.isExpired()) {
|
||||||
|
loggedIn = true;
|
||||||
|
String targetUrl = signServiceTicketFor(tgt.getId(), service);
|
||||||
|
if (targetUrl == null) {
|
||||||
|
// 无 service 或 service 非法(未注册/校验被拒):回默认落点
|
||||||
|
targetUrl = buildDefaultFallbackUrl(request);
|
||||||
|
}
|
||||||
|
url = targetUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.warn("check-sso 预检异常,按未登录降级处理", e);
|
||||||
|
loggedIn = false;
|
||||||
|
url = "";
|
||||||
|
}
|
||||||
|
data.put("loggedIn", loggedIn);
|
||||||
|
data.put("url", url);
|
||||||
|
return buildResponse(0, data, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为已持有的 TGT 向指定 service 签发 ServiceTicket,返回带 ticket 的回跳地址;
|
||||||
|
* 无 service / service 未注册(UnauthorizedServiceException)/ 其它异常返回 null。
|
||||||
|
* 注意:这里不做防暴破介入——凭有效 TGT 签 ST 是 CAS 原生单点行为,无需验证码。
|
||||||
|
*/
|
||||||
|
private String signServiceTicketFor(String tgtId, String service) {
|
||||||
|
if (service == null || service.trim().isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
try {
|
||||||
|
service = URLDecoder.decode(service, "UTF-8");
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.warn("service 参数 URL 解码失败,按原值使用: {}", service);
|
||||||
|
}
|
||||||
|
Service svc = new WebApplicationServiceFactory().createService(service.trim());
|
||||||
|
if (svc == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
ServiceTicket st = centralAuthenticationService.grantServiceTicket(tgtId, svc, null);
|
||||||
|
String svcUrl = svc.getId();
|
||||||
|
return svcUrl + (svcUrl.contains("?") ? "&" : "?") + "ticket=" + st.getId();
|
||||||
|
} catch (UnauthorizedServiceException e) {
|
||||||
|
logger.warn("check-sso:service 未在注册表中授权: {}", service);
|
||||||
|
return null;
|
||||||
|
} catch (AbstractTicketException | AuthenticationException e) {
|
||||||
|
logger.warn("check-sso:签发 ServiceTicket 失败", e);
|
||||||
|
return null;
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("check-sso:签发 ServiceTicket 异常", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/public-key")
|
@GetMapping("/public-key")
|
||||||
public JSONObject publicKey() {
|
public JSONObject publicKey() {
|
||||||
if (rsaPublicKey == null || rsaPublicKey.trim().isEmpty()) {
|
if (rsaPublicKey == null || rsaPublicKey.trim().isEmpty()) {
|
||||||
@@ -305,7 +383,7 @@ public class LoginController {
|
|||||||
writeJson(response, buildResponse(1, data, msg));
|
writeJson(response, buildResponse(1, data, msg));
|
||||||
} else {
|
} else {
|
||||||
// 原生表单提交:302 回登录页并带错误提示;同时让浏览器在成功时记住密码。
|
// 原生表单提交:302 回登录页并带错误提示;同时让浏览器在成功时记住密码。
|
||||||
String target = "/front/index.html?error=" + URLEncoder.encode(msg, "UTF-8");
|
String target = "/cas/login?error=" + URLEncoder.encode(msg, "UTF-8");
|
||||||
response.sendRedirect(target);
|
response.sendRedirect(target);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -424,13 +502,7 @@ public class LoginController {
|
|||||||
}
|
}
|
||||||
// 返回绝对 URL,确保前端无论从哪个 origin 打开都能正确跳回 CAS 同源,
|
// 返回绝对 URL,确保前端无论从哪个 origin 打开都能正确跳回 CAS 同源,
|
||||||
// 从而把 TGC Cookie 一起带过去(相对路径 /cas/login 在非同源前端下会跳错 origin)。
|
// 从而把 TGC Cookie 一起带过去(相对路径 /cas/login 在非同源前端下会跳错 origin)。
|
||||||
// 防御:默认落点若未配置、或误配成 CAS 原生登录页路径,则强制回退到前端首页,
|
return buildDefaultFallbackUrl(request);
|
||||||
// 避免出现“登录成功却被甩回 /cas/login”的死循环(曾因 application.yml 误配踩过)。
|
|
||||||
String target = defaultRedirectUrl;
|
|
||||||
if (target == null || target.trim().isEmpty() || target.contains("/cas/login")) {
|
|
||||||
target = "/front/index.html";
|
|
||||||
}
|
|
||||||
return buildCasAbsoluteUrl(request, target);
|
|
||||||
} catch (AuthenticationException | AbstractTicketException e) {
|
} catch (AuthenticationException | AbstractTicketException e) {
|
||||||
logger.error("用户 {} 建立 SSO 会话失败", username, e);
|
logger.error("用户 {} 建立 SSO 会话失败", username, e);
|
||||||
return null;
|
return null;
|
||||||
@@ -440,6 +512,19 @@ public class LoginController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造默认落点(无 service / service 未注册 / 未配置时的回退地址)。
|
||||||
|
* 默认落点若未配置、或误配成 CAS 原生登录页路径,则强制回退到业务系统首页,
|
||||||
|
* 避免出现“登录成功却被甩回 /cas/login”的死循环(曾因 application.yml 误配踩过)。
|
||||||
|
*/
|
||||||
|
private String buildDefaultFallbackUrl(HttpServletRequest request) {
|
||||||
|
String target = defaultRedirectUrl;
|
||||||
|
if (target == null || target.trim().isEmpty() || target.contains("/cas/login")) {
|
||||||
|
target = "https://base.yunhaixingtong.com";
|
||||||
|
}
|
||||||
|
return buildCasAbsoluteUrl(request, target);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 基于当前请求推断 CAS 自身的访问基地址(scheme://host:port),用于构造绝对跳转 URL,
|
* 基于当前请求推断 CAS 自身的访问基地址(scheme://host:port),用于构造绝对跳转 URL,
|
||||||
* 避免前端页面不在 CAS 同源时相对路径跳转错 origin,导致 TGC Cookie 无法随跳转携带。
|
* 避免前端页面不在 CAS 同源时相对路径跳转错 origin,导致 TGC Cookie 无法随跳转携带。
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
# 注册自定义 Nacos 配置注入器:在 Spring Boot 环境准备阶段把 Nacos 配置注入 Environment
|
|
||||||
org.springframework.boot.env.EnvironmentPostProcessor=\
|
|
||||||
org.apereo.cas.web.custom.config.nacos.NacosEnvironmentPostProcessor
|
|
||||||
@@ -100,11 +100,13 @@ custom:
|
|||||||
CQIDAQAB
|
CQIDAQAB
|
||||||
private-key: MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCrK1seBBZlCaTCanpadxVpSZQ2ATFgMHRKnyHWp/Ip+59tfYwQzpTNI9AcjC6sWqTNweWN2sYMJdZnZe/xzwLbv4w48t/hXJhDuKe+q2+YTBLpzeSbhCpBV2uHQMmNViGA7yt4TwuGUMVz1WH0XDJJvylsLUQzSzaU3qKDv/01Hz4KrFbrWS77tVicvNxGClzRwXcr+ZDACNV/WkqMa08bWaks1welPbKnm73nQZ8W12piAC17uTgIOXAwMp+l/7Y+DmRkDOiA7kNOr9CiU5aqNl+BPbAE2lhVP7eCVuGFFtz1Pf9KoV5P0qVdxqYFcu8ojpa4rGUAR4pOKixSAwoJAgMBAAECggEBAJsnYJ9zmw0Y0bS8qY2I9eqLHIJbUHNdWHwvhI+7ikHN6HRp4eV2K+FK/iByXPgz7cDoafjMfsqSHad5+iZYu8GO/j68wHzZ3tc78Sle4nfNNTOjQv5Irbiu0jQ2YLvxf8sMgH28QLkOS/y0RecnDxpT4etBmNlZ6u7EY9FC3PUhkTCb2Ioy+xhA9FNZLc9awmzuqLZMkhUThspRmEFoBDgQV/U0o9OVVi3nEecEUvOCQpv3kwpDZMJECNPxY+qnGs3uKl6hbWXVhhqO9XKOu3wN2AJmakugcBSD6NpFu+C+BOah/kMlcrUXgVQUeObhwXfQjrw4CNyrD1AVvtVWfwECgYEA4Q38R60oZtZqEXz2KvhOM4uvD3def+IrLa6xnjHtqOQ1m7Ok1wLvhVW3P37/HyYyuq3EhRo2drD1qI58wZ9QAvFSnrNMHwqbaYew4OciFs28zPcYgRR1dLEOcz/WBbqhixClrJX22imil/eqxktDamDetSlkKo9F+rInzQukWykCgYEAwrSWRGLjHdQYmeBShOFKb+OPAR2198QTZPN5DmNXVgRfp+cb7FQ7Mw8ZndpDlfu7ARTwhNv0y3piCj8bJtm7BgXaQowwTR0AxRDkqF6fUNVqKEyE25G1hhE2NCD/4b+rAO8IaCCNnjSvAWhXjg7sQHBngJhFxYY68NQx6wuv8+ECgYA9KcLda/bl8/HBrPdt+CuwoI7EIknhcYMQmECXYLRZSSQ0m575/EdCMmhp7kbBnWz+BQm6rsQMf81TsN3A9V4WXqV4kfvnF8fi50iv5sEB94oPHjxfoELHovLtaXTt+6m5lJ2iCOOcqOIScPHNkFjTCV6dV5Fsa6G2H0Ln2/kf+QKBgBJVZMzdrtsRLJvYpoyCljrA56dIP+krmlPoJR74tbGQyd0KgvOW3tgo9NWLRs/pmys36InqGDvMs0yZspnO6LBhfrLDdozcTUVBGPbNqhy3a76Nrfjq2HfeSQdRkCaWoKh+2YDMh+k8trngNFZZC1/p9aUDMZau0//3SxgByrihAoGAPyOKhvPuoQfkbe6kyqGOZoPmctr5mQiHAPzNoKQkGD0pAGrId77KfDjPY8qM0ZTCLuYUJHy3ISefgHKANHLBgPlQasHo6t2f5Wk/173ju+cruW07APe3imXeGkpL4cg4n1YWvSAkwr4GoDXvTd6uL15zM452oxcW8p4hdoITaP0=
|
private-key: MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCrK1seBBZlCaTCanpadxVpSZQ2ATFgMHRKnyHWp/Ip+59tfYwQzpTNI9AcjC6sWqTNweWN2sYMJdZnZe/xzwLbv4w48t/hXJhDuKe+q2+YTBLpzeSbhCpBV2uHQMmNViGA7yt4TwuGUMVz1WH0XDJJvylsLUQzSzaU3qKDv/01Hz4KrFbrWS77tVicvNxGClzRwXcr+ZDACNV/WkqMa08bWaks1welPbKnm73nQZ8W12piAC17uTgIOXAwMp+l/7Y+DmRkDOiA7kNOr9CiU5aqNl+BPbAE2lhVP7eCVuGFFtz1Pf9KoV5P0qVdxqYFcu8ojpa4rGUAR4pOKixSAwoJAgMBAAECggEBAJsnYJ9zmw0Y0bS8qY2I9eqLHIJbUHNdWHwvhI+7ikHN6HRp4eV2K+FK/iByXPgz7cDoafjMfsqSHad5+iZYu8GO/j68wHzZ3tc78Sle4nfNNTOjQv5Irbiu0jQ2YLvxf8sMgH28QLkOS/y0RecnDxpT4etBmNlZ6u7EY9FC3PUhkTCb2Ioy+xhA9FNZLc9awmzuqLZMkhUThspRmEFoBDgQV/U0o9OVVi3nEecEUvOCQpv3kwpDZMJECNPxY+qnGs3uKl6hbWXVhhqO9XKOu3wN2AJmakugcBSD6NpFu+C+BOah/kMlcrUXgVQUeObhwXfQjrw4CNyrD1AVvtVWfwECgYEA4Q38R60oZtZqEXz2KvhOM4uvD3def+IrLa6xnjHtqOQ1m7Ok1wLvhVW3P37/HyYyuq3EhRo2drD1qI58wZ9QAvFSnrNMHwqbaYew4OciFs28zPcYgRR1dLEOcz/WBbqhixClrJX22imil/eqxktDamDetSlkKo9F+rInzQukWykCgYEAwrSWRGLjHdQYmeBShOFKb+OPAR2198QTZPN5DmNXVgRfp+cb7FQ7Mw8ZndpDlfu7ARTwhNv0y3piCj8bJtm7BgXaQowwTR0AxRDkqF6fUNVqKEyE25G1hhE2NCD/4b+rAO8IaCCNnjSvAWhXjg7sQHBngJhFxYY68NQx6wuv8+ECgYA9KcLda/bl8/HBrPdt+CuwoI7EIknhcYMQmECXYLRZSSQ0m575/EdCMmhp7kbBnWz+BQm6rsQMf81TsN3A9V4WXqV4kfvnF8fi50iv5sEB94oPHjxfoELHovLtaXTt+6m5lJ2iCOOcqOIScPHNkFjTCV6dV5Fsa6G2H0Ln2/kf+QKBgBJVZMzdrtsRLJvYpoyCljrA56dIP+krmlPoJR74tbGQyd0KgvOW3tgo9NWLRs/pmys36InqGDvMs0yZspnO6LBhfrLDdozcTUVBGPbNqhy3a76Nrfjq2HfeSQdRkCaWoKh+2YDMh+k8trngNFZZC1/p9aUDMZau0//3SxgByrihAoGAPyOKhvPuoQfkbe6kyqGOZoPmctr5mQiHAPzNoKQkGD0pAGrId77KfDjPY8qM0ZTCLuYUJHy3ISefgHKANHLBgPlQasHo6t2f5Wk/173ju+cruW07APe3imXeGkpL4cg4n1YWvSAkwr4GoDXvTd6uL15zM452oxcW8p4hdoITaP0=
|
||||||
login:
|
login:
|
||||||
# 登录成功(无 service 参数)时的默认落点。必须是静态前端页,绝不能指向 CAS 原生登录页
|
# 登录成功(无 service 参数)时的默认落点(业务系统首页)。
|
||||||
|
# 必须是绝对 URL(http(s)://...),绝不能指向 CAS 原生登录页
|
||||||
# (/cas/login 或 /login)——否则登录成功会被甩回 CAS 登录页形成死循环。
|
# (/cas/login 或 /login)——否则登录成功会被甩回 CAS 登录页形成死循环。
|
||||||
# 该值会被打包进 WAR 的 classpath application.yml,且会覆盖 LoginController 中 @Value 的默认值,
|
# 该值会被打包进 WAR 的 classpath application.yml,且会覆盖 LoginController 中 @Value 的默认值,
|
||||||
# 所以这里必须显式写成前端页路径。业务系统带 service 参数时仍走各自回跳地址。
|
# 所以这里必须显式写成业务系统首页地址。业务系统带 service 参数时仍走各自回跳地址。
|
||||||
default-redirect-url: /front/index.html
|
# 注意:生产环境由 Nacos cas.yaml 的 custom.login.default-redirect-url 覆盖(优先级更高),两处需保持一致。
|
||||||
|
default-redirect-url: https://base.yunhaixingtong.com
|
||||||
# 是否强制每次登录都必须校验验证码。
|
# 是否强制每次登录都必须校验验证码。
|
||||||
# false(默认)/ 不配置:维持原逻辑,仅当同一会话登录失败达到阈值后才要求验证码。
|
# false(默认)/ 不配置:维持原逻辑,仅当同一会话登录失败达到阈值后才要求验证码。
|
||||||
# true:任何登录(含首次)都必须携带并校验验证码,用于彻底阻断批量账号枚举探测。
|
# true:任何登录(含首次)都必须携带并校验验证码,用于彻底阻断批量账号枚举探测。
|
||||||
|
|||||||
Reference in New Issue
Block a user