NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript==
// @name XJTLU UIM 自动登录 (请求用户名+密码+OTP)
// @namespace http://tampermonkey.net/
// @version 2.0
// @description XJTLU UIM 自动登录增强版,自动完成辅助验证,无需手动输入验证码/OTP -- 2.0已部分重构,提升成功率和安全性
// @author wujinjun
// @license MIT
// @match *://example.com
// @match https://uim.xjtlu.edu.cn/esc-sso/login/page
// @match https://uim.xjtlu.edu.cn/login/mfaLogin.html
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// @grant GM_registerMenuCommand
// @require https://cdn.jsdelivr.net/npm/jsencrypt@3.5.4/bin/jsencrypt.min.js
// @require https://update.greasyfork.org/scripts/511697/1471164/TOTP%20Generator.js
// @run-at document-start
// ==/UserScript==
(function() {
'use strict';
// **********************************
// *********** 配置区 *************
// **********************************
const CONFIG_KEY = "xjtlu_uim_autologin_config_v2";
const ENABLED_KEY = "xjtlu_uim_autologin_enabled";
const REFERER_KEY = 'xjtlu_uim_original_referer';
const DEFAULT_CONFIG = {
enabled: true,
username: "username",
password: "password",
secret: "OTP",
forceOtpAfterSuccess: false,
maxRetries1: 3,
retryDelayMs1: 2000,
maxRetries2: 3,
retryDelayMs2: 2000
};
function loadConfig() {
const saved = GM_getValue(CONFIG_KEY, {});
return {
...DEFAULT_CONFIG,
...(saved && typeof saved === "object" ? saved : {})
};
}
function saveConfig(nextConfig) {
const merged = { ...loadConfig(), ...nextConfig };
GM_setValue(CONFIG_KEY, merged);
GM_setValue(ENABLED_KEY, !!merged.enabled);
return merged;
}
function setScriptEnabled(enabled) {
const nextEnabled = !!enabled;
GM_setValue(ENABLED_KEY, nextEnabled);
config.enabled = nextEnabled;
GM_setValue(CONFIG_KEY, config);
return nextEnabled;
}
function promptConfigValue(key, label, currentValue) {
const displayed = String(currentValue ?? "");
const answer = prompt(`${label}\n当前值: ${displayed || "(空)"}\n请输入新值,取消则不修改:`, `${displayed === "[hidden]" ? "" : displayed}`);
if (answer === null) {
return null;
}
const trimmed = answer.trim();
if (!trimmed) {
return null;
}
config = saveConfig({ [key]: trimmed });
return trimmed;
}
function promptBooleanConfigValue(key, label, currentValue) {
const current = !!currentValue;
const answer = prompt(`${label}\n当前值: ${current ? "true" : "false"}\n请输入 true / false,取消则不修改:`, current ? "true" : "false");
if (answer === null) {
return null;
}
const normalized = String(answer).trim().toLowerCase();
if (normalized !== "true" && normalized !== "false") {
alert("请输入 true 或 false");
return null;
}
const value = normalized === "true";
config = saveConfig({ [key]: value });
return value;
}
function promptNumberConfigValue(key, label, currentValue, minValue = 0) {
const displayed = String(currentValue ?? "");
const answer = prompt(`${label}\n当前值: ${displayed}\n请输入整数值,取消则不修改:`, displayed);
if (answer === null) {
return null;
}
const trimmed = String(answer).trim();
if (!/^\d+$/.test(trimmed)) {
alert("请输入一个有效的非负整数");
return null;
}
const value = Number(trimmed);
if (value < minValue) {
alert(`值不能小于 ${minValue}`);
return null;
}
config = saveConfig({ [key]: value });
return value;
}
function registerConfigMenu() {
if (typeof GM_registerMenuCommand !== "function") {
return;
}
GM_registerMenuCommand(`XJTLU UIM: 自动登录 ${scriptEnabled ? '✅':'❌'}`, () => {
const nextEnabled = !scriptEnabled;
setScriptEnabled(nextEnabled);
alert(nextEnabled ? "已开启 XJTLU UIM 自动登录" : "已关闭 XJTLU UIM 自动登录,可在菜单中重新打开");
console.log(`脚本已${nextEnabled ? "开启" : "关闭"}。`);
window.location.reload();
});
GM_registerMenuCommand("XJTLU UIM: 设置用户名", () => {
const value = promptConfigValue("username", "设置用户名", config.username);
if (value !== null) {
alert(`用户名已更新为: ${value}`);
}
});
GM_registerMenuCommand("XJTLU UIM: 设置密码", () => {
const value = promptConfigValue("password", "设置明文密码", "[hidden]");
if (value !== null) {
alert("密码已更新");
}
});
GM_registerMenuCommand("XJTLU UIM: 设置OTP密钥", () => {
const value = promptConfigValue("secret", "设置 TOTP 密钥", config.secret);
if (value !== null) {
alert("OTP 密钥已更新");
}
});
GM_registerMenuCommand("XJTLU UIM: 设置强制OTP", () => {
const value = promptBooleanConfigValue("forceOtpAfterSuccess", "强制在首次登录成功后执行 OTP(true/false)", config.forceOtpAfterSuccess);
if (value !== null) {
alert(`已更新 forceOtpAfterSuccess = ${value}`);
}
});
GM_registerMenuCommand("XJTLU UIM: 设置阶段1最大重试", () => {
const value = promptNumberConfigValue("maxRetries1", "设置阶段1最大重试次数", config.maxRetries1, 0);
if (value !== null) {
alert(`已更新 maxRetries1 = ${value}`);
}
});
GM_registerMenuCommand("XJTLU UIM: 设置阶段1重试延迟(ms)", () => {
const value = promptNumberConfigValue("retryDelayMs1", "设置阶段1重试延迟(毫秒)", config.retryDelayMs1, 0);
if (value !== null) {
alert(`已更新 retryDelayMs1 = ${value}`);
}
});
GM_registerMenuCommand("XJTLU UIM: 设置阶段2最大重试", () => {
const value = promptNumberConfigValue("maxRetries2", "设置阶段2最大重试次数", config.maxRetries2, 0);
if (value !== null) {
alert(`已更新 maxRetries2 = ${value}`);
}
});
GM_registerMenuCommand("XJTLU UIM: 设置阶段2重试延迟(ms)", () => {
const value = promptNumberConfigValue("retryDelayMs2", "设置阶段2重试延迟(毫秒)", config.retryDelayMs2, 0);
if (value !== null) {
alert(`已更新 retryDelayMs2 = ${value}`);
}
});
}
let config = loadConfig();
let scriptEnabled = !!GM_getValue(ENABLED_KEY, config.enabled);
config.enabled = scriptEnabled;
const username = config.username;
const password = config.password;
const secret = config.secret;
const FORCE_OTP_AFTER_SUCCESS = !!config.forceOtpAfterSuccess;
const MAX_RETRIES_1 = Number(config.maxRetries1) || 3;
const RETRY_DELAY_MS_1 = Number(config.retryDelayMs1) || 2000;
const MAX_RETRIES_2 = Number(config.maxRetries2) || 3;
const RETRY_DELAY_MS_2 = Number(config.retryDelayMs2) || 2000;
const isLoginPage = window.location.href.includes("/esc-sso/login/page");
const isMfaPage = window.location.href.includes("/login/mfaLogin.html");
const isSettingDummy = window.location.href.includes("example.com");
const nowtimestamp = Date.now();
registerConfigMenu();
if (!scriptEnabled) {
console.warn("XJTLU UIM 自动登录已关闭,脚本不执行任何请求。可在 Tampermonkey 菜单中重新打开。");
return;
}
// =========================================================================
// Referer 捕获与重定向逻辑 (使用 Tampermonkey 存储跨页面持久化 Referer)
// =========================================================================
if (isLoginPage) {
// 处于 /esc-sso/login/page 时,捕获并存储原始 Referer
if (document.referrer && !document.referrer.includes("xjtlu.edu.cn")) {
// 只有当 Referer 不是来自本域时才存储,避免循环
GM_setValue(REFERER_KEY, document.referrer);
console.log("已捕获并存储原始 Referer:", document.referrer);
} else {
// 如果是直接访问或来自本域,清除旧的 Referer
GM_deleteValue(REFERER_KEY);
}
} else if (isMfaPage) {
// 处于 /login/mfaLogin.html 时,尝试跳转回原始 Referer
const originalReferer = GM_getValue(REFERER_KEY, null);
if (originalReferer) {
console.warn(`检测到被重定向到MFA页面,且找到原始 Referer。正在跳转回: ${originalReferer}`);
// 清除存储的 Referer,防止后续意外跳转
GM_deleteValue(REFERER_KEY);
// 执行跳转,替换当前历史记录
window.location.replace(originalReferer);
// 停止脚本后续执行,因为页面已跳转
return;
} else {
// 报告未找到 Referer,但允许脚本继续执行阶段2登录请求
console.log("处于MFA页面,未找到原始 Referer,脚本将尝试执行阶段2登录请求。");
}
} else if (isSettingDummy) {
// 处于 `*://example.com` 时,视为"离线模式"配置脚本功能,立即退出
return;
}
// 如果页面已被跳转 (或在 MFA 页面上需要回跳),则停止执行后续的登录请求逻辑
if (isMfaPage && GM_getValue(REFERER_KEY, null)) {
return;
}
// 预处理 (在 Referer 逻辑后重新定义,确保变量初始化)
let publicKey = null;
let publicKeyId = null;
// RSA 加密函数
function rsaEncrypt(text, pubKey) {
try {
const encrypt = new JSEncrypt();
encrypt.setPublicKey(pubKey);
return encrypt.encrypt(text);
} catch (e) {
console.error("RSA加密失败:", e);
return null;
}
}
// --- 登录请求逻辑 ---
async function fetchJson(url, options = {}) {
const response = await fetch(url, {
credentials: "include",
mode: "cors",
cache: "no-store",
headers: {
"Accept": "application/json, text/plain, */*",
"X-Requested-With": "XMLHttpRequest",
"Origin": "https://uim.xjtlu.edu.cn",
"Referer": window.location.href
},
...options // 这是Javascript object spread,不是错误。 Ref: https://stackoverflow.com/questions/171251/how-can-i-merge-properties-of-two-javascript-objects
});
const responseText = await response.text();
let parsed = null;
if (responseText) {
try {
parsed = JSON.parse(responseText);
} catch (e) {
console.warn("非JSON响应:", responseText);
}
}
return {
ok: response.ok,
status: response.status,
statusText: response.statusText,
responseText,
data: parsed,
headers: response.headers
};
}
// 步骤1: 获取公钥和公钥ID
fetchJson(`https://uim.xjtlu.edu.cn/esc-sso/api/v3/auth/policy?_=${nowtimestamp}`)
.then(({ data, ok, status, statusText }) => {
if (!ok || !data || !data.data || !data.data.param) {
console.error("获取公钥请求失败!", status, statusText, data);
return;
}
try {
publicKey = data.data.param.publicKey;
publicKeyId = data.data.param.publicKeyId;
if (publicKey && publicKeyId) {
console.log("成功获取公钥和公钥ID。");
const encryptedPassword = rsaEncrypt(password, publicKey);
sendLoginRequest1(encryptedPassword, publicKeyId, 1);
} else {
console.error("未能获取公钥或公钥ID。", data);
}
} catch (e) {
console.error("解析阶段1的响应数据失败:", e, data);
}
})
.catch(error => {
console.error("获取公钥请求失败!", error);
});
// 步骤2: 发送第一个登录请求(使用密码)
function sendLoginRequest1(encryptedPassword, publicKeyId, attempt = 1) {
const requestData = {
"authType": "webLocalAuth",
"dataField": {
"username": username,
"password": encryptedPassword,
"publicKeyId": publicKeyId
}
};
fetchJson(`https://uim.xjtlu.edu.cn/esc-sso/api/v3/auth/doLogin?_=${nowtimestamp}`, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(requestData)
}).then(({ data, ok, status, statusText, responseText }) => {
console.log(`阶段1第 ${attempt} 次登录请求(密码)发送成功。`);
console.log("原始响应数据:", responseText);
try {
const data_code = data && data.code;
const data_msg = data && data.msg;
console.log(`解析响应数据: 响应码(code): ${data_code}; 消息(msg): ${data_msg}`);
// 【检查访问过快 (429)】
if (data_code === "429") {
if (attempt < MAX_RETRIES_1) {
const nextAttempt = attempt + 1;
console.warn(`[重试 ${attempt}/${MAX_RETRIES_1}] 检测到访问过快 (429)。将在 ${RETRY_DELAY_MS_1 / 1000} 秒后重试...`);
setTimeout(() => {
sendLoginRequest1(encryptedPassword, publicKeyId, nextAttempt);
}, RETRY_DELAY_MS_1);
return;
} else {
console.error(`访问过快 (429) 错误:已达到最大重试次数 (${MAX_RETRIES_1}),停止尝试。`);
return;
}
}
// 【检查会话过期】如果过期,强制立即执行第二次登录(OTP)。
if (data_msg && data_msg.includes("当前会话已过期")) {
console.warn("检测到会话过期,强制立即执行阶段2登录请求(OTP)。", data_msg);
sendLoginRequest2();
return;
}
// 【滑动验证码】退出,用户自行操作
if (data_code === "SSO10094" || data_msg && (data_msg.includes("slider verification") || data_msg.includes("滑块验证码"))) {
console.error("检测到滑块验证码,请自行完成操作,稍后若遇到OTP,可刷新以重新触发此脚本。 " + data_msg);
alert("[XJTLU UIM 自动登录] 检测到滑块验证码,请自行完成操作,稍后若遇到OTP,可刷新以重新触发此脚本。 " + data_msg);
return;
}
// 【用户名或密码错误】立即退出并永久关闭此脚本功能,随后在 Tampermonkey 菜单中重新开启
if (data_code === "SSO10002" || (data_msg && (data_msg.includes("Incorrect Username or Password") || data_msg.includes("用户名或密码错误")))) {
console.error("❌用户名或密码错误,脚本立即关闭并停用,可在 Tampermonkey 菜单中重新打开 ", data_msg);
alert("[XJTLU UIM 自动登录] ❌用户名或密码错误,脚本立即关闭并停用,可在 Tampermonkey 菜单中重新打开 " + data_msg);
setScriptEnabled(false);
return;
}
// 【其他错误(非0)】
if (data_code !== "0") {
console.error("阶段1 其他错误(返回值非0),请检查用户名/密码是否正确");
alert("[XJTLU UIM 自动登录] 阶段1 其他错误(返回值非0),请检查用户名/密码是否正确");
return;
}
// 检查是否需要OTP(标准流程:返回mfaLogin.html重定向)
const isMfaRequired = data && data.data && data.data.redirect && data.data.redirect.includes("/mfaLogin.html");
if (isMfaRequired) {
console.log("检测到MFA登录。");
console.log("继续执行阶段2登录请求(OTP)。");
sendLoginRequest2();
} else if (FORCE_OTP_AFTER_SUCCESS) {
// 【强制执行逻辑】如果配置了强制执行
console.log("登录成功,但配置开启了FORCE_OTP_AFTER_SUCCESS。");
console.log("强制执行阶段2登录请求(OTP)。");
sendLoginRequest2();
} else {
// 登录直接成功(标准流程)
console.log("登录直接成功或无需MFA。");
}
} catch (e) {
console.error("解析阶段1登录请求的响应数据失败。请检查原始数据是否为有效JSON。", e);
}
}).catch(error => {
console.error("阶段1登录请求失败!", status, statusText, error);
});
}
async function postJsonWithBrowserSession(url, payload) {
const response = await fetch(url, {
method: "POST",
credentials: "include",
mode: "cors",
cache: "no-store",
headers: {
"Content-Type": "application/json",
"Accept": "application/json, text/plain, */*",
"X-Requested-With": "XMLHttpRequest",
"Origin": "https://uim.xjtlu.edu.cn",
"Referer": window.location.href
},
body: JSON.stringify(payload)
});
const responseText = await response.text();
let parsed = null;
if (responseText) {
try {
parsed = JSON.parse(responseText);
} catch (e) {
console.warn("非JSON响应:", responseText);
}
}
return {
ok: response.ok,
status: response.status,
statusText: response.statusText,
responseText,
data: parsed,
headers: response.headers
};
}
// 步骤3: 发送第二个登录请求 (使用OTP)
function sendLoginRequest2(attempt = 1) {
const nowtimestamp2 = Date.now();
const authType2 = "webOtpAuth";
// 使用外部库提供的 generateTOTP(secret)
generateTOTP(secret)
.then(async otp => {
console.log("已生成OTP:", otp);
const requestData2 = {
"authType": authType2,
"dataField": {
"username": username,
"otp": otp
},
"redirectUri": ""
};
const url = `https://uim.xjtlu.edu.cn/esc-sso/api/v3/auth/doLogin?_=${nowtimestamp2}`;
try {
const response = await postJsonWithBrowserSession(url, requestData2);
console.log("阶段2登录请求(OTP)发送成功。");
console.log("原始响应数据:", response.responseText);
const data = response.data || {};
const data_code = data.code;
const data_msg = data && data.msg;
console.log(`解析响应数据: 响应码(code): ${data_code}; 消息(msg): ${data_msg}`);
// 【检查访问过快 (429)】
if (data_code === "429") {
if (attempt < MAX_RETRIES_2) {
const nextAttempt = attempt + 1;
console.warn(`[重试 ${attempt}/${MAX_RETRIES_2}] 检测到访问过快 (429)。将在 ${RETRY_DELAY_MS_2 / 1000} 秒后重试...`);
setTimeout(() => {
sendLoginRequest2(nextAttempt);
}, RETRY_DELAY_MS_2);
return;
} else {
console.error(`访问过快 (429) 错误:已达到最大重试次数 (${MAX_RETRIES_2}),停止尝试。`);
return;
}
}
// 【滑动验证码】退出,用户自行操作
if (data_code === "SSO10094" || (data_msg && (data_msg.includes("slider verification") || data_msg.includes("滑块验证码")))) {
console.error("检测到滑块验证码,请自行完成操作,稍后若遇到OTP,可刷新以重新触发此脚本。 " + data_msg);
alert("[XJTLU UIM 自动登录] 检测到滑块验证码,请自行完成操作,稍后若遇到OTP,可刷新以重新触发此脚本。 " + data_msg);
return;
}
// 【其他错误(非0)】
if (data_code !== "0") {
console.error("阶段2 其他错误(返回值非0),请检查用户名/TOTP密钥是否正确");
alert("[XJTLU UIM 自动登录] 阶段2 其他错误(返回值非0),请检查用户名/TOTP密钥是否正确");
return;
}
// 检查服务器是否返回了最终的重定向链接 (无论是 data.data.redirect 还是顶层 redirect)
let finalRedirectUrl = (data.data && data.data.redirect) || data.redirect;
if (finalRedirectUrl) {
// 1. 服务器返回了链接,使用服务器的链接进行跳转
console.warn(`登录成功。使用服务器返回的链接进行跳转: ${finalRedirectUrl}`);
GM_deleteValue(REFERER_KEY);
window.location.replace(finalRedirectUrl);
} else {
// 2. 服务器未返回链接,尝试使用存储的 Referer 进行回跳
const originalReferer = GM_getValue(REFERER_KEY, null);
if (originalReferer) {
console.warn(`服务器未返回最终跳转链接,尝试使用存储的原始 Referer 进行回跳: ${originalReferer}`);
GM_deleteValue(REFERER_KEY);
window.location.replace(originalReferer);
} else {
console.error("阶段2登录请求成功,但未找到最终跳转链接(服务器响应或存储的Referer)。用户可能停留在当前页面。");
GM_deleteValue(REFERER_KEY); // 即使失败也清除,避免干扰下次登录
}
}
} catch (error) {
console.error("解析阶段2登录请求的响应数据失败。尝试使用存储的Referer进行回跳。", error);
const originalReferer = GM_getValue(REFERER_KEY, null);
if (originalReferer) {
console.warn(`OTP POST 失败,但发现存储的 Referer。正在手动跳转到: ${originalReferer}`);
GM_deleteValue(REFERER_KEY);
window.location.replace(originalReferer);
}
}
})
.catch(error => {
console.error("生成OTP失败:", error);
});
}
})();