+2
-2
@@ -1,5 +1,5 @@
|
||||
# 前端由 CasSsoFront 仓库独立发布,本后端仓库只发纯后端(cas.war)
|
||||
src/main/webapp/front/
|
||||
# 登录页静态资源(src/main/webapp/front/)随 WAR 打包提供:其中 native-login.js 等
|
||||
# 仅存在于后端仓,独立的 CasSsoFront 静态仓没有,必须入库才能保证 CI 构建的 war 自包含
|
||||
*.iml
|
||||
.idea/
|
||||
# 构建产物与二进制不入库(CI 用 maven 从源码构建 cas.war)
|
||||
|
||||
@@ -8,7 +8,7 @@ CAS 5.3.16 单点登录服务(war overlay 项目,源自 CasSsoService 改版
|
||||
- **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 开。
|
||||
- **pom 不声明外网仓库**:依赖全部经 nexus maven-public 代理解析(内网 CI 无外网)。
|
||||
- 前端登录页静态文件由 CasSsoFront 仓库独立发布(`src/main/webapp/front/` 不入库)。
|
||||
- **登录页静态文件随 WAR 打包**:`src/main/webapp/front/`(logo、native-login.js 等)入库并打进 cas.war,由 CAS 同源托管在 `/cas/front/*`。原独立的 CasSsoFront 静态仓**已废弃不用**——其中没有 `native-login.js`(登录逻辑只在本仓),Caddy 磁盘托管仅为灰度兜底。
|
||||
|
||||
## 推送后验证
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
@@ -0,0 +1,164 @@
|
||||
function BschLogin(ssoServerHost) {
|
||||
this.isDebug = false;
|
||||
this.ssoServerHost = ssoServerHost;
|
||||
this.captchaUrl = ssoServerHost + "api/captcha";
|
||||
this.loginUrl = ssoServerHost + "api/login";
|
||||
this.publicKeyUrl = ssoServerHost + "api/public-key";
|
||||
this.needCaptchaUrl = ssoServerHost + "api/need-captcha";
|
||||
this.publicKey = "";
|
||||
// this.setCookie("loginPageTag", loginPageTag, 30);
|
||||
}
|
||||
|
||||
BschLogin.prototype = {
|
||||
getPublicKey: function () {
|
||||
var _this = this;
|
||||
if (_this.isDebug) {
|
||||
_this.publicKey =
|
||||
"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqytbHgQWZQmkwmp6WncVaUmUNgExYDB0Sp8h1qfyKfufbX2MEM6UzSPQHIwurFqkzcHljdrGDCXWZ2Xv8c8C27+MOPLf4VyYQ7invqtvmEwS6c3km4QqQVdrh0DJjVYhgO8reE8LhlDFc9Vh9FwySb8pbC1EM0s2lN6ig7/9NR8+CqxW61ku+7VYnLzcRgpc0cF3K/mQwAjVf1pKjGtPG1mpLNcHpT2yp5u950GfFtdqYgAte7k4CDlwMDKfpf+2Pg5kZAzogO5DTq/QolOWqjZfgT2wBNpYVT+3glbhhRbc9T3/SqFeT9KlXcamBXLvKI6WuKxlAEeKTiosUgMKCQIDAQAB";
|
||||
return;
|
||||
}
|
||||
$.get(
|
||||
_this.publicKeyUrl,
|
||||
function (resp) {
|
||||
_this.publicKey = resp.data.publicKey;
|
||||
},
|
||||
"json",
|
||||
);
|
||||
},
|
||||
//是否需要验证校验码
|
||||
isNeedCaptcha: function (callback) {
|
||||
if (this.isDebug) return callback(false);
|
||||
$.get(
|
||||
this.needCaptchaUrl,
|
||||
function (resp) {
|
||||
if (resp.code == 0) {
|
||||
var needCaptcha = resp.data.needCaptcha;
|
||||
callback(needCaptcha);
|
||||
} else {
|
||||
callback(true);
|
||||
}
|
||||
},
|
||||
"json",
|
||||
);
|
||||
},
|
||||
|
||||
//登录
|
||||
login: function (username, pwd, captcha, onSuccess, onFailed) {
|
||||
var service = this.getQueryVariable("service");
|
||||
var encrypt = new JSEncrypt();
|
||||
encrypt.setPublicKey(this.publicKey);
|
||||
username = encrypt.encrypt(username);
|
||||
pwd = encrypt.encrypt(pwd);
|
||||
|
||||
var params = {
|
||||
loginname: username,
|
||||
pwd: pwd,
|
||||
captcha: captcha,
|
||||
};
|
||||
if (typeof service === "string") params.service = service;
|
||||
console.log(JSON.stringify(params));
|
||||
|
||||
$.ajax({
|
||||
url: this.loginUrl,
|
||||
type: "POST",
|
||||
data: params,
|
||||
dataType: "json",
|
||||
success: function (resp, textStatus, jqXHR) {
|
||||
if (resp && typeof resp.code !== "undefined") {
|
||||
if (resp.code == 0) {
|
||||
var url = (resp.data && resp.data.url) || "";
|
||||
// 兜底:后端未返回有效地址或返回了 CAS 原生登录页路径时,跳回同源根
|
||||
if (!url || url.indexOf("/cas/login") === 0) {
|
||||
url = "/";
|
||||
}
|
||||
console.log("login success, redirect =>", url);
|
||||
onSuccess(url);
|
||||
} else {
|
||||
var needCaptcha = resp.data ? resp.data.needCaptcha : false;
|
||||
onFailed(resp.msg, needCaptcha);
|
||||
}
|
||||
return;
|
||||
}
|
||||
onSuccess(jqXHR.responseURL || "");
|
||||
},
|
||||
error: function (jqXHR) {
|
||||
var resp = jqXHR.responseJSON;
|
||||
if (resp && typeof resp.code !== "undefined") {
|
||||
var needCaptcha = resp.data ? resp.data.needCaptcha : false;
|
||||
onFailed(resp.msg, needCaptcha);
|
||||
return;
|
||||
}
|
||||
onFailed("登录请求失败", false);
|
||||
},
|
||||
});
|
||||
},
|
||||
//请求短信验证码
|
||||
requestSmsCode: function (phoneCode, onSuccess, onFailed) {
|
||||
var params = {
|
||||
phone: phoneCode,
|
||||
};
|
||||
|
||||
$.get(
|
||||
this.smsCodeUrl,
|
||||
params,
|
||||
function (resp) {
|
||||
if (resp.code == 0) {
|
||||
onSuccess();
|
||||
} else {
|
||||
onFailed(resp.msg);
|
||||
}
|
||||
},
|
||||
"json",
|
||||
);
|
||||
},
|
||||
|
||||
//按短信验证码,修改密码
|
||||
updatePwd: function (phoneCode, smsCode, newPwd, onSuccess, onFailed) {
|
||||
var params = {
|
||||
phone: phoneCode,
|
||||
smsCode: smsCode,
|
||||
newPwd: newPwd,
|
||||
};
|
||||
|
||||
$.post(
|
||||
this.updatePwdUrl,
|
||||
params,
|
||||
function (resp) {
|
||||
if (resp.code == 0) {
|
||||
onSuccess();
|
||||
} else {
|
||||
onFailed(resp.msg);
|
||||
}
|
||||
},
|
||||
"json",
|
||||
);
|
||||
},
|
||||
|
||||
captchaSrc: function () {
|
||||
return this.captchaUrl + "?k=" + new Date().getTime();
|
||||
},
|
||||
|
||||
getQueryVariable: function (paramName) {
|
||||
var query = window.location.search.substring(1);
|
||||
var params = query.split("&");
|
||||
for (var i = 0; i < params.length; i++) {
|
||||
var param = params[i];
|
||||
|
||||
var pos = param.indexOf("=");
|
||||
if (pos == -1) continue;
|
||||
|
||||
var key = param.slice(0, pos);
|
||||
var value = param.slice(pos + 1, param.length);
|
||||
|
||||
if (key == paramName) return value;
|
||||
}
|
||||
return "";
|
||||
},
|
||||
|
||||
setCookie: function (c_name, value, expiredays) {
|
||||
var exdate = new Date();
|
||||
exdate.setDate(exdate.getDate() + expiredays);
|
||||
document.cookie =
|
||||
c_name + "=" + escape(value) + (expiredays == null ? "" : ";expires=" + exdate.toGMTString()) + ";path=/"; //+";domain=";
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
// CAS context-path 为根 "/",前端由 CAS 同源托管在 /front/,接口在 /api/*。
|
||||
// 用同源相对路径,避免写死 host/协议,且天然规避跨域(LoginController 未开 CORS)。
|
||||
window.ssoServerHost = "/cas/";
|
||||
|
||||
window.beianText = "©2021 bestsch.com inc All Rights reserved";
|
||||
window.beianLink = "【沪ICP备13000443号-1】";
|
||||
window.logoHeight = 35;
|
||||
+2
File diff suppressed because one or more lines are too long
@@ -0,0 +1,199 @@
|
||||
/**
|
||||
* CAS 原生登录页(templates/casLoginView.html)配套脚本。
|
||||
*
|
||||
* 页面由 /cas/login 直接渲染,逻辑分三步:
|
||||
* 1. onload 先调 GET {ssoServerHost}api/check-sso 预检:
|
||||
* - 已有有效 TGT 且带 service → 直接跳 data.url(service?ticket=ST-xxx),单点免密回业务系统;
|
||||
* - 已有有效 TGT 无 service → 跳默认落点(/cas/front/index.html);
|
||||
* - 未登录/接口异常 → 继续初始化登录表单(fail 降级保证灰度期旧后端也能用)。
|
||||
* 2. 初始化:公钥加载、验证码显隐、error= 查询串回显历史错误。
|
||||
* 3. 提交:走 bschlogin.login()(RSA+POST /cas/api/login),成功触发"记住密码"iframe 后
|
||||
* JS 手动跳 data.url;失败展示 msg,且 needCaptcha=true 时自动展开验证码面板。
|
||||
*
|
||||
* 资源与接口全部使用 conf.js 的 window.ssoServerHost("/cas/"),同源相对,避免跨域。
|
||||
*/
|
||||
|
||||
(function () {
|
||||
var bschLogin;
|
||||
|
||||
function setWarning(msg) {
|
||||
document.getElementById("warningLabel").textContent = msg || "";
|
||||
}
|
||||
|
||||
function setCaptcha(isVisible) {
|
||||
var panel = document.getElementById("captchaPanel");
|
||||
if (isVisible) {
|
||||
document.getElementById("captchaImg").src = bschLogin.captchaSrc();
|
||||
panel.style.display = "block";
|
||||
panel.dataset.captcha = "true";
|
||||
} else {
|
||||
panel.style.display = "none";
|
||||
panel.dataset.captcha = "false";
|
||||
}
|
||||
}
|
||||
|
||||
function refreshCaptcha() {
|
||||
document.getElementById("captchaImg").src = bschLogin.captchaSrc();
|
||||
}
|
||||
|
||||
function initLoginForm() {
|
||||
bschLogin.getPublicKey();
|
||||
bschLogin.isNeedCaptcha(setCaptcha);
|
||||
var err = bschLogin.getQueryVariable("error");
|
||||
if (err) {
|
||||
// 非 AJAX 提交失败时后端会 302 回 /cas/login?error=<encode后的提示>
|
||||
try {
|
||||
err = decodeURIComponent(err);
|
||||
} catch (e) {
|
||||
/* 保持原值 */
|
||||
}
|
||||
setWarning(err);
|
||||
}
|
||||
document.getElementById("user_input").focus();
|
||||
}
|
||||
|
||||
// SSO 预检:命中有效会话直接免密跳转,这是恢复单点登录的关键一步
|
||||
function checkSsoBeforeForm(service, done) {
|
||||
if (!$ || !$.get) {
|
||||
done();
|
||||
return;
|
||||
}
|
||||
$.get(
|
||||
window.ssoServerHost + "api/check-sso",
|
||||
service ? { service: service } : {},
|
||||
function (resp) {
|
||||
if (resp && resp.code == 0 && resp.data && resp.data.loggedIn && resp.data.url) {
|
||||
console.log("check-sso hit sso session, redirect =>", resp.data.url);
|
||||
window.location.replace(resp.data.url);
|
||||
return;
|
||||
}
|
||||
done();
|
||||
},
|
||||
"json",
|
||||
).fail(done);
|
||||
}
|
||||
|
||||
function triggerPasswordSave(username, pwd, encUser, encPwd, captcha) {
|
||||
try {
|
||||
document.getElementById("pm_user").value = username;
|
||||
document.getElementById("pm_pwd").value = pwd;
|
||||
document.getElementById("pm_loginname").value = encUser;
|
||||
document.getElementById("pm_pwd_enc").value = encPwd;
|
||||
document.getElementById("pm_captcha").value = captcha;
|
||||
document.getElementById("pm_service").value =
|
||||
document.getElementById("service_hidden").value;
|
||||
document.getElementById("pwdMgrForm").submit();
|
||||
} catch (e) {
|
||||
/* 记住密码属最佳努力,失败不影响主流程 */
|
||||
}
|
||||
}
|
||||
|
||||
function doLogin() {
|
||||
var username = document.getElementById("user_input").value.trim();
|
||||
var pwd = document.getElementById("password_input").value;
|
||||
var captcha = document.getElementById("captcha_input").value;
|
||||
var needCaptcha = document.getElementById("captchaPanel").dataset.captcha === "true";
|
||||
|
||||
if (!username) {
|
||||
setWarning("请输入用户名!");
|
||||
return;
|
||||
}
|
||||
if (!pwd) {
|
||||
setWarning("请输入密码!");
|
||||
return;
|
||||
}
|
||||
if (needCaptcha && !captcha) {
|
||||
setWarning("请输入验证码!");
|
||||
return;
|
||||
}
|
||||
if (!bschLogin.publicKey) {
|
||||
setWarning("公钥加载中,请稍候重试");
|
||||
return;
|
||||
}
|
||||
|
||||
// 隐藏 iframe 的"记住密码"提交要求 loginname/pwd 为 RSA 密文(后端必填),这里预生成
|
||||
var encUser = null,
|
||||
encPwd = null;
|
||||
try {
|
||||
var jsenc = new JSEncrypt();
|
||||
jsenc.setPublicKey(bschLogin.publicKey);
|
||||
encUser = jsenc.encrypt(username);
|
||||
encPwd = jsenc.encrypt(pwd);
|
||||
} catch (e) {
|
||||
/* 最佳努力:失败仅影响记住密码,不影响主流程 */
|
||||
}
|
||||
|
||||
var btn = document.getElementById("loginBtn");
|
||||
btn.disabled = true;
|
||||
btn.textContent = "登录中...";
|
||||
setWarning("");
|
||||
|
||||
bschLogin.login(
|
||||
username,
|
||||
pwd,
|
||||
captcha,
|
||||
function (url) {
|
||||
// 成功:最佳努力让浏览器记住密码(隐藏 iframe 真实提交),再手动跳转
|
||||
triggerPasswordSave(username, pwd, encUser, encPwd, captcha);
|
||||
// 页面存在 #loginSuccess 遮罩(完整版视觉)则展示;否则退回文字提示
|
||||
var sc = document.getElementById("loginSuccess");
|
||||
if (sc) {
|
||||
sc.style.display = "flex";
|
||||
} else {
|
||||
setWarning("登录成功,正在跳转…");
|
||||
}
|
||||
setTimeout(function () {
|
||||
window.location.href = url;
|
||||
}, 450);
|
||||
},
|
||||
function (msg, nc) {
|
||||
btn.disabled = false;
|
||||
btn.textContent = "登录";
|
||||
setWarning(msg || "登录失败");
|
||||
if (nc) {
|
||||
setCaptcha(true);
|
||||
} else if (needCaptcha) {
|
||||
// 验证码一次性消费,无论是否要求都必须换一张
|
||||
refreshCaptcha();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
window.onload = function () {
|
||||
// 占位保留:bschlogin.login() 内部自行读取 service 参数并提交给后端
|
||||
document.getElementById("service_hidden").value =
|
||||
bschLogin.getQueryVariable("service") || "";
|
||||
|
||||
checkSsoBeforeForm(document.getElementById("service_hidden").value, initLoginForm);
|
||||
};
|
||||
|
||||
// 暴露给模板内联 onclick 使用
|
||||
window.doLogin = doLogin;
|
||||
window.refreshCaptcha = refreshCaptcha;
|
||||
|
||||
window.onerror = function (msg) {
|
||||
setWarning("页面脚本异常:" + msg);
|
||||
return false;
|
||||
};
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
bschLogin = new BschLogin(window.ssoServerHost);
|
||||
|
||||
document.getElementById("user_input").addEventListener("keydown", function (e) {
|
||||
if (e.key === "Enter") document.getElementById("password_input").focus();
|
||||
});
|
||||
document.getElementById("password_input").addEventListener("keydown", function (e) {
|
||||
if (e.key === "Enter") {
|
||||
if (document.getElementById("captchaPanel").style.display !== "none") {
|
||||
document.getElementById("captcha_input").focus();
|
||||
} else {
|
||||
doLogin();
|
||||
}
|
||||
}
|
||||
});
|
||||
document.getElementById("captcha_input").addEventListener("keydown", function (e) {
|
||||
if (e.key === "Enter") doLogin();
|
||||
});
|
||||
});
|
||||
})();
|
||||
+6
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user