SSO 地址去掉 /cas 前缀:context-path 改为根路径,登录地址 /cas/login 变为 /login
build / build-push (push) Successful in 3m33s

This commit is contained in:
2026-09-03 22:28:28 +08:00
parent 229067d518
commit 6ceaed63d6
12 changed files with 57 additions and 37 deletions
+2 -2
View File
@@ -6,9 +6,9 @@ CAS 5.3.16 单点登录服务(war overlay 项目,源自 CasSsoService 改版
- **构建产物是 WAR 不是 jar**:pom `packaging=war`、`finalName=cas`,CI 在 maven 构建后 `cp cas.war app.jar`(可执行 WAR,WarLauncher 直接 `java -jar`)。 - **构建产物是 WAR 不是 jar**:pom `packaging=war`、`finalName=cas`,CI 在 maven 构建后 `cp cas.war app.jar`(可执行 WAR,WarLauncher 直接 `java -jar`)。
- **Dockerfile 为 CAS 专用**:镜像内生成自签名 keystore(8443 HTTPS),内置 `docker/cas.properties`(仅 TLS/端口/日志)与 `etc/cas/config/log4j2.xml`;业务配置由 projman 配置管理挂载 `/app/config/application.yml`。 - **Dockerfile 为 CAS 专用**:镜像内生成自签名 keystore(8443 HTTPS),内置 `docker/cas.properties`(仅 TLS/端口/日志)与 `etc/cas/config/log4j2.xml`;业务配置由 projman 配置管理挂载 `/app/config/application.yml`。
- **健康检查**:`/cas/status`(HTTPS 自签名),projman 应用需配置 healthPath=`/cas/status` + HTTPS 开。 - **健康检查**:`/status`(HTTPS 自签名),projman 应用需配置 healthPath=`/status` + HTTPS 开。(context-path 已改为根 `/`,登录地址为 `https://sso.yunhaixingtong.com/login`,不再有 `/cas` 前缀;健康检查从 `/cas/status` 变为 `/status`。)
- **pom 不声明外网仓库**:依赖全部经 nexus maven-public 代理解析(内网 CI 无外网)。 - **pom 不声明外网仓库**:依赖全部经 nexus maven-public 代理解析(内网 CI 无外网)。
- **登录页静态文件随 WAR 打包**:`src/main/webapp/front/`(logo、native-login.js 等)入库并打进 cas.war,由 CAS 同源托管在 `/cas/front/*`。原独立的 CasSsoFront 静态仓**已废弃不用**——其中没有 `native-login.js`(登录逻辑只在本仓),Caddy 磁盘托管仅为灰度兜底。 - **登录页静态文件随 WAR 打包**:`src/main/webapp/front/`(logo、native-login.js 等)入库并打进 cas.war,由 CAS 同源托管在 `/front/*`。原独立的 CasSsoFront 静态仓**已废弃不用**——其中没有 `native-login.js`(登录逻辑只在本仓)。
## 推送后验证 ## 推送后验证
+3 -2
View File
@@ -2,8 +2,9 @@
@set JAVA_ARGS=-Xms500m -Xmx1g @set JAVA_ARGS=-Xms500m -Xmx1g
@if "%CAS_SERVER_NAME%" == "" set CAS_SERVER_NAME=http://localhost:8443 @if "%CAS_SERVER_NAME%" == "" set CAS_SERVER_NAME=http://localhost:8443
@if "%CAS_SERVER_PREFIX%" == "" set CAS_SERVER_PREFIX=http://localhost:8443/cas @if "%CAS_SERVER_PREFIX%" == "" set CAS_SERVER_PREFIX=http://localhost:8443
@set CAS_JAVA_OVERRIDES=-Dserver.use-forward-headers=false -Dcas.server.name=%CAS_SERVER_NAME% -Dcas.server.prefix=%CAS_SERVER_PREFIX% @rem 根 context-path 必须用空字符串(Boot 1.5 显式拒绝 "/":Root ContextPath must be specified using an empty string)
@set CAS_JAVA_OVERRIDES=-Dserver.use-forward-headers=false -Dserver.context-path= -Dcas.server.name=%CAS_SERVER_NAME% -Dcas.server.prefix=%CAS_SERVER_PREFIX%
@set CAS_DIR=\etc\cas @set CAS_DIR=\etc\cas
@set CONFIG_DIR=\etc\cas\config @set CONFIG_DIR=\etc\cas\config
+1 -1
View File
@@ -1,7 +1,7 @@
# CAS 5.3.16 运行镜像(单阶段,war 预构建) # CAS 5.3.16 运行镜像(单阶段,war 预构建)
# CI 已先 mvn package 并在容器内把产物 cp 成固定名 target/app.jar(= cas.war,可执行 WAR) # CI 已先 mvn package 并在容器内把产物 cp 成固定名 target/app.jar(= cas.war,可执行 WAR)
# 构建上下文 = 仓库根:docker build -f docker/Dockerfile . # 构建上下文 = 仓库根:docker build -f docker/Dockerfile .
# 健康检查(projman 应用配置 healthPath=/cas/status + HTTPS 自签名开):curl -fk https://localhost:8443/cas/status # 健康检查(projman 应用配置 healthPath=/status + HTTPS 自签名开):curl -fk https://localhost:8443/status
# 时区 Asia/Shanghai;JAVA_OPTS 由 projman 部署 env 注入(默认 -Xms512m -Xmx1024m) # 时区 Asia/Shanghai;JAVA_OPTS 由 projman 部署 env 注入(默认 -Xms512m -Xmx1024m)
FROM eclipse-temurin:8-jre FROM eclipse-temurin:8-jre
@@ -10,7 +10,7 @@ import org.springframework.context.annotation.Configuration;
/** /**
* 将自定义数据库认证处理器注册进 CAS 认证执行计划, * 将自定义数据库认证处理器注册进 CAS 认证执行计划,
* 使标准登录表单(/cas/login)的认证链包含数据库校验。 * 使标准登录表单(/login)的认证链包含数据库校验。
*/ */
@Configuration("DbAuthenticationConfiguration") @Configuration("DbAuthenticationConfiguration")
public class DbAuthenticationConfiguration implements AuthenticationEventExecutionPlanConfigurer { public class DbAuthenticationConfiguration implements AuthenticationEventExecutionPlanConfigurer {
@@ -20,7 +20,7 @@ import java.sql.PreparedStatement;
import java.sql.ResultSet; import java.sql.ResultSet;
/** /**
* 自定义数据库认证处理器:让 CAS 标准登录表单(/cas/login)也走数据库校验。 * 自定义数据库认证处理器:让 CAS 标准登录表单(/login)也走数据库校验。
* 校验逻辑与 /api/login 完全一致(BCrypt + sys_user 表),保证两套登录行为统一。 * 校验逻辑与 /api/login 完全一致(BCrypt + sys_user 表),保证两套登录行为统一。
* 用原生 DriverManager 直连,不依赖 CAS 内置 JdbcAuthenticationHandler 的连接池/模板, * 用原生 DriverManager 直连,不依赖 CAS 内置 JdbcAuthenticationHandler 的连接池/模板,
* 因此不受 WAR 中 spring-jdbc 双版本等潜在初始化问题影响。 * 因此不受 WAR 中 spring-jdbc 双版本等潜在初始化问题影响。
@@ -386,7 +386,7 @@ public class LoginController {
writeJson(response, buildResponse(1, data, msg)); writeJson(response, buildResponse(1, data, msg));
} else { } else {
// 原生表单提交:302 回登录页并带错误提示;同时让浏览器在成功时记住密码。 // 原生表单提交:302 回登录页并带错误提示;同时让浏览器在成功时记住密码。
String target = "/cas/login?error=" + URLEncoder.encode(msg, "UTF-8"); String target = "/login?error=" + URLEncoder.encode(msg, "UTF-8");
response.sendRedirect(target); response.sendRedirect(target);
} }
} }
@@ -504,7 +504,7 @@ public class LoginController {
return svcUrl + (svcUrl.contains("?") ? "&" : "?") + "ticket=" + st.getId(); return svcUrl + (svcUrl.contains("?") ? "&" : "?") + "ticket=" + st.getId();
} }
// 返回绝对 URL,确保前端无论从哪个 origin 打开都能正确跳回 CAS 同源, // 返回绝对 URL,确保前端无论从哪个 origin 打开都能正确跳回 CAS 同源,
// 从而把 TGC Cookie 一起带过去(相对路径 /cas/login 在非同源前端下会跳错 origin)。 // 从而把 TGC Cookie 一起带过去(相对路径 /login 在非同源前端下会跳错 origin)。
return buildDefaultFallbackUrl(request); return buildDefaultFallbackUrl(request);
} catch (AuthenticationException | AbstractTicketException e) { } catch (AuthenticationException | AbstractTicketException e) {
logger.error("用户 {} 建立 SSO 会话失败", username, e); logger.error("用户 {} 建立 SSO 会话失败", username, e);
@@ -518,16 +518,29 @@ public class LoginController {
/** /**
* 构造默认落点(无 service / service 未注册 / 未配置时的回退地址)。 * 构造默认落点(无 service / service 未注册 / 未配置时的回退地址)。
* 默认落点若未配置、或误配成 CAS 原生登录页路径,则强制回退到业务系统首页, * 默认落点若未配置、或误配成 CAS 原生登录页路径,则强制回退到业务系统首页,
* 避免出现“登录成功却被甩回 /cas/login”的死循环(曾因 application.yml 误配踩过)。 * 避免出现“登录成功却被甩回登录页”的死循环(曾因 application.yml 误配踩过)。
*/ */
private String buildDefaultFallbackUrl(HttpServletRequest request) { private String buildDefaultFallbackUrl(HttpServletRequest request) {
String target = defaultRedirectUrl; String target = defaultRedirectUrl;
if (target == null || target.trim().isEmpty() || target.contains("/cas/login")) { if (target == null || target.trim().isEmpty() || isCasLoginPath(target)) {
target = "https://base.yunhaixingtong.com"; target = "https://base.yunhaixingtong.com";
} }
return buildCasAbsoluteUrl(request, target); return buildCasAbsoluteUrl(request, target);
} }
/**
* 判断地址是否指向 CAS 自身登录页(剥掉查询串后以 /login 或 /cas/login 结尾,后者兼容历史配置),
* 用于拦截误把登录页配成默认落点导致的“登录成功却被甩回登录页”死循环。
*/
private boolean isCasLoginPath(String url) {
String path = url;
int q = path.indexOf('?');
if (q >= 0) {
path = path.substring(0, q);
}
return path.endsWith("/login") || path.endsWith("/cas/login");
}
/** /**
* 基于当前请求推断 CAS 自身的访问基地址(scheme://host:port),用于构造绝对跳转 URL, * 基于当前请求推断 CAS 自身的访问基地址(scheme://host:port),用于构造绝对跳转 URL,
* 避免前端页面不在 CAS 同源时相对路径跳转错 origin,导致 TGC Cookie 无法随跳转携带。 * 避免前端页面不在 CAS 同源时相对路径跳转错 origin,导致 TGC Cookie 无法随跳转携带。
+12 -6
View File
@@ -3,21 +3,27 @@ server:
enabled: false enabled: false
port: 8443 port: 8443
use-forward-headers: false use-forward-headers: false
# context-path 必须为根(登录地址 https://sso.yunhaixingtong.com/login,去掉 /cas 前缀)。
# 注意:不能在这里写 server.context-path —— overlay WAR 自带的 classpath application.properties
# 显式设了 server.context-path=/cas,同在 WAR 内时 properties 优先级高于 yml,写了会被覆盖。
# 且 Boot 1.5 规定根 context 必须用空字符串("/" 会抛 Root ContextPath must be specified
# using an empty string)。因此该键只能由外部提供:本地 build.cmd 以 -Dserver.context-path=
# 注入(系统属性优先级最高),生产由 projman 挂载的外部配置(优先于 WAR 内 classpath)设空值。
cas: cas:
# 显式声明 CAS 自身访问地址,避免根路径 / 或 /index.html 被重定向到错误端口(曾出现跳到 8080 的问题)。 # 显式声明 CAS 自身访问地址,避免根路径 / 或 /index.html 被重定向到错误端口(曾出现跳到 8080 的问题)。
# CAS 的默认值是 https://cas.example.org:8443,若运行时被外部配置覆盖成 8080,根页面重定向就会错位。 # CAS 的默认值是 https://cas.example.org:8443,若运行时被外部配置覆盖成 8080,根页面重定向就会错位。
# prefix 不带 /cas(与根 context-path 一致),CAS 内部生成的登录/注销等 URL 均由它派生。
server: server:
name: http://localhost:8443 name: http://localhost:8443
prefix: http://localhost:8443/cas prefix: http://localhost:8443
# CAS 核心配置 # CAS 核心配置
tgc: tgc:
secure: false secure: false
# 显式指定 TGC Cookie 的 Path,必须覆盖 /cas 下所有路径(含自定义 /cas/api/login)。 # 显式指定 TGC Cookie 的 Path。context-path 为根后,登录页在 /login、接口在 /api/*,
# 若不配置,CAS 默认会把 Path 留空,浏览器按“请求目录”取值: # TGC 必须对全站可见,故固定 Path=/。若留空,浏览器按“请求目录”取值,行为依赖隐式推导,不可靠。
# - 标准登录在 /cas/login 种 Cookie -> Path=/cas/ -> 正常 # (历史版本曾配 /cas 以兼容 /cas/api/login 种 Cookie,context-path 改根后同步改为 /。)
# - 自定义登录在 /cas/api/login 种 Cookie -> Path=/cas/api/ -> 跳到 /cas/login 时浏览器不带此 Cookie,导致还要再登录 path: /
path: /cas
# 固定 TGC 加密/签名密钥。不配置的话每次重启都会自动生成新密钥, # 固定 TGC 加密/签名密钥。不配置的话每次重启都会自动生成新密钥,
# 导致浏览器里旧 TGC Cookie 解密失败(表现为启动后首次访问的 NPE DEBUG 噪声),且启动日志反复 WARN。 # 导致浏览器里旧 TGC Cookie 解密失败(表现为启动后首次访问的 NPE DEBUG 噪声),且启动日志反复 WARN。
crypto: crypto:
@@ -4,7 +4,7 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<title>请稍候</title> <title>请稍候</title>
<link rel="icon" type="image/x-icon" href="/cas/front/image/favicon.ico?v=1" /> <link rel="icon" type="image/x-icon" href="/front/image/favicon.ico?v=1" />
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" /> <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
<meta http-equiv="Pragma" content="no-cache" /> <meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Expires" content="0" /> <meta http-equiv="Expires" content="0" />
@@ -12,17 +12,17 @@
<body> <body>
<!-- <!--
覆盖 CAS 原生 casGenericSuccessView.html:已有有效 TGT 后再访问 /cas/login(无 service) 覆盖 CAS 原生 casGenericSuccessView.html:已有有效 TGT 后再访问 /login(无 service)
时,CAS 不再渲染默认"您已经成功登录中央认证系统"页,而是自动跳转到配置的默认落点 时,CAS 不再渲染默认"您已经成功登录中央认证系统"页,而是自动跳转到配置的默认落点
(custom.login.default-redirect-url,即 base.yunhaixingtong.com)。 (custom.login.default-redirect-url,即 base.yunhaixingtong.com)。
跳转地址复用 /cas/api/check-sso 的返回(跟随 Nacos 配置,不硬编码);接口异常时兜底。 跳转地址复用 /api/check-sso 的返回(跟随 Nacos 配置,不硬编码);接口异常时兜底。
--> -->
<script> <script>
(function () { (function () {
var fallback = "https://base.yunhaixingtong.com"; var fallback = "https://base.yunhaixingtong.com";
try { try {
var xhr = new XMLHttpRequest(); var xhr = new XMLHttpRequest();
xhr.open("GET", "/cas/api/check-sso", true); xhr.open("GET", "/api/check-sso", true);
xhr.onreadystatechange = function () { xhr.onreadystatechange = function () {
if (xhr.readyState !== 4) return; if (xhr.readyState !== 4) return;
try { try {
@@ -8,13 +8,13 @@
<meta http-equiv="Pragma" content="no-cache" /> <meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Expires" content="0" /> <meta http-equiv="Expires" content="0" />
<title>云海教育 · 统一身份认证平台</title> <title>云海教育 · 统一身份认证平台</title>
<link rel="icon" type="image/x-icon" href="/cas/front/image/favicon.ico?v=1" /> <link rel="icon" type="image/x-icon" href="/front/image/favicon.ico?v=1" />
<link rel="stylesheet" href="/cas/front/css/login.css?v=1.5" /> <link rel="stylesheet" href="/front/css/login.css?v=1.5" />
<script src="/cas/front/libs/jquery.min.js?v=2.1"></script> <script src="/front/libs/jquery.min.js?v=2.1"></script>
<script src="/cas/front/js/jsencrypt.min.js?v=2.1"></script> <script src="/front/js/jsencrypt.min.js?v=2.1"></script>
<script src="/cas/front/js/conf.js?v=2.3"></script> <script src="/front/js/conf.js?v=2.3"></script>
<script src="/cas/front/js/bschlogin.js?v=2.1"></script> <script src="/front/js/bschlogin.js?v=2.1"></script>
<script src="/cas/front/js/native-login.js?v=3.2"></script> <script src="/front/js/native-login.js?v=3.2"></script>
</head> </head>
<body> <body>
@@ -22,7 +22,7 @@
<aside class="login-banner"> <aside class="login-banner">
<div class="banner-inner"> <div class="banner-inner">
<div class="brand"> <div class="brand">
<img class="brand-logo" src="/cas/front/image/logo.png" alt="logo" /> <img class="brand-logo" src="/front/image/logo.png" alt="logo" />
<span class="brand-name">云海教育</span> <span class="brand-name">云海教育</span>
<span class="brand-slogan">云海星通</span> <span class="brand-slogan">云海星通</span>
</div> </div>
+1 -1
View File
@@ -68,7 +68,7 @@ BschLogin.prototype = {
if (resp.code == 0) { if (resp.code == 0) {
var url = (resp.data && resp.data.url) || ""; var url = (resp.data && resp.data.url) || "";
// 兜底:后端未返回有效地址或返回了 CAS 原生登录页路径时,跳回同源根 // 兜底:后端未返回有效地址或返回了 CAS 原生登录页路径时,跳回同源根
if (!url || url.indexOf("/cas/login") === 0) { if (!url || url.indexOf("/cas/login") === 0 || url.indexOf("/login") === 0) {
url = "/"; url = "/";
} }
console.log("login success, redirect =>", url); console.log("login success, redirect =>", url);
+2 -2
View File
@@ -1,6 +1,6 @@
// CAS context-path 为根 "/",前端由 CAS 同源托管在 /front/,接口在 /api/*。 // context-path 为根 "/",前端由 CAS 同源托管在 /front/,接口在 /api/*。
// 用同源相对路径,避免写死 host/协议,且天然规避跨域(LoginController 未开 CORS)。 // 用同源相对路径,避免写死 host/协议,且天然规避跨域(LoginController 未开 CORS)。
window.ssoServerHost = "/cas/"; window.ssoServerHost = "/";
window.beianText = "©2021 bestsch.com inc All Rights reserved"; window.beianText = "©2021 bestsch.com inc All Rights reserved";
window.beianLink = "【沪ICP备13000443号-1】"; window.beianLink = "【沪ICP备13000443号-1】";
+5 -5
View File
@@ -1,16 +1,16 @@
/** /**
* CAS 原生登录页(templates/casLoginView.html)配套脚本。 * CAS 原生登录页(templates/casLoginView.html)配套脚本。
* *
* 页面由 /cas/login 直接渲染,逻辑分三步: * 页面由 /login 直接渲染,逻辑分三步:
* 1. onload 先调 GET {ssoServerHost}api/check-sso 预检: * 1. onload 先调 GET {ssoServerHost}api/check-sso 预检:
* - 已有有效 TGT 且带 service → 直接跳 data.url(service?ticket=ST-xxx),单点免密回业务系统; * - 已有有效 TGT 且带 service → 直接跳 data.url(service?ticket=ST-xxx),单点免密回业务系统;
* - 已有有效 TGT 无 service → 跳默认落点(/cas/front/index.html); * - 已有有效 TGT 无 service → 跳默认落点(Nacos 配置 custom.login.default-redirect-url);
* - 未登录/接口异常 → 继续初始化登录表单(fail 降级保证灰度期旧后端也能用)。 * - 未登录/接口异常 → 继续初始化登录表单(fail 降级保证灰度期旧后端也能用)。
* 2. 初始化:公钥加载、验证码显隐、error= 查询串回显历史错误。 * 2. 初始化:公钥加载、验证码显隐、error= 查询串回显历史错误。
* 3. 提交:走 bschlogin.login()(RSA+POST /cas/api/login),成功后 JS 直接跳 data.url; * 3. 提交:走 bschlogin.login()(RSA+POST /api/login),成功后 JS 直接跳 data.url;
* 失败展示 msg,且 needCaptcha=true 时自动展开验证码面板。 * 失败展示 msg,且 needCaptcha=true 时自动展开验证码面板。
* *
* 资源与接口全部使用 conf.js 的 window.ssoServerHost("/cas/"),同源相对,避免跨域。 * 资源与接口全部使用 conf.js 的 window.ssoServerHost("/"),同源相对,避免跨域。
*/ */
(function () { (function () {
@@ -71,7 +71,7 @@
bschLogin.isNeedCaptcha(setCaptcha); bschLogin.isNeedCaptcha(setCaptcha);
var err = bschLogin.getQueryVariable("error"); var err = bschLogin.getQueryVariable("error");
if (err) { if (err) {
// 非 AJAX 提交失败时后端会 302 回 /cas/login?error=<encode后的提示> // 非 AJAX 提交失败时后端会 302 回 /login?error=<encode后的提示>
try { try {
err = decodeURIComponent(err); err = decodeURIComponent(err);
} catch (e) { } catch (e) {