喵喵的新公司使用的阿里云的日志SLS平台和监控Arms平台,当然如果只使用这俩没什么问题,但是当你的公司存在海外站和国内站的时候,来回切换登录就是很难受的一件事情了。

今天你打开海外站阿里云,登陆了上去,看了一下日志没什么问题。突然,国内服务报异常,你打开日志详情,发现没有权限查看日志信息(因为现在登陆的是国外站的账号),你点退出登陆重新打开国内日志详情,发现跳到国外站的登陆了。你焦头烂额内心OS:为什么国内站的日志详情不跳到国内站的登陆,反而跳转到国外站。你手动输入国内站的登陆地址,输入账号密码,又到了两步验证,你掏出手机,费劲找到了一次性验证码,登陆成功之后再次打开报警的日志详情,你才真正看到了日志。

PixPin_2026-06-02_10-27-26

这一套也太痛苦了,所以我基于油猴开发了这个脚本。

可以快速切换不同的站点,并且支持离线解析OTP CODE

PixPin_2026-06-02_10-39-57

使用工具识别你的二维码,把二维码的内容粘贴进去,就可以快速复制我们的OTP CODE了

PixPin_2026-06-02_10-55-24

当然也可以直接把二维码放到剪切板里,然后自动识别来填充

并且脚本除了使用扫描二维码的库之外,完全不需要联网,可以保证两步验证的安全

PixPin_2026-06-02_10-58-11

把下面的脚步放到你的油猴里面就可以食用了

// ==UserScript==
// @name 阿里云国内/国际站切换 + MFA自动填充
// @namespace http://tampermonkey.net/
// @version 2.2.0
// @description 在阿里云国内站和国际站之间快速切换;登录时支持手动输入MFA密钥、扫码提取或剪贴板识别二维码,一键填充验证码
// @author You
// @match https://*.aliyun.com/*
// @match https://*.alibabacloud.com/*
// @icon https://img.alicdn.com/tfs/TB1_ZXuNcfpK1RjSZFOXXa6nFXa-32-32.ico
// @require https://cdn.jsdelivr.net/npm/jsqr@1.4.0/dist/jsQR.min.js
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// @run-at document-end
// ==/UserScript==

(function () {
'use strict';

const currentHost = location.hostname;

// ── Domain detection ──────────────────────────────────────────────
const isAliyun = currentHost.endsWith('.aliyun.com') || currentHost === 'aliyun.com';
const isAliCloud = currentHost.endsWith('.alibabacloud.com') || currentHost === 'alibabacloud.com';
if (!isAliyun && !isAliCloud) return;

const domainKey = isAliyun ? 'aliyun.com' : 'alibabacloud.com';
const STORAGE_KEY = 'mfa_secret_' + domainKey;

// ═══════════════════════════════════════════════════════════════════
// 1. Site Switcher Button
// ═══════════════════════════════════════════════════════════════════
function initSiteSwitcher() {
let targetHost, label;
if (isAliyun) {
targetHost = currentHost.replace(/\.?aliyun\.com$/, currentHost.startsWith('aliyun.com') ? 'alibabacloud.com' : '.alibabacloud.com');
label = '切换国际站';
} else {
targetHost = currentHost.replace(/\.?alibabacloud\.com$/, currentHost.startsWith('alibabacloud.com') ? 'aliyun.com' : '.aliyun.com');
label = '切换国内站';
}

const targetUrl = location.protocol + '//' + targetHost + location.pathname + location.search + location.hash;

const btn = document.createElement('div');
btn.textContent = label;
btn.style.cssText = 'position:fixed;right:12px;bottom:120px;z-index:99999;padding:8px 14px;background:rgba(255,106,0,0.85);color:#fff;font-size:13px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;border-radius:6px;cursor:pointer;box-shadow:0 2px 8px rgba(0,0,0,.15);transition:background 0.2s,transform 0.15s;user-select:none;white-space:nowrap;';
btn.addEventListener('mouseenter', function () { this.style.background = 'rgba(255,106,0,1)'; this.style.transform = 'scale(1.05)'; });
btn.addEventListener('mouseleave', function () { this.style.background = 'rgba(255,106,0,0.85)'; this.style.transform = 'scale(1)'; });
btn.addEventListener('click', function () {
if (confirm('将跳转到: ' + targetUrl)) {
location.href = targetUrl;
}
});
document.body.appendChild(btn);
}

// ═══════════════════════════════════════════════════════════════════
// 2. TOTP Implementation (RFC 6238)
// ═══════════════════════════════════════════════════════════════════
function base32ToBytes(base32) {
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
base32 = base32.replace(/=+$/, '').toUpperCase();
const bits = base32.length * 5;
const bytes = new Uint8Array(Math.floor(bits / 8));
let byteIdx = 0, buf = 0, bufBits = 0;
for (let i = 0; i < base32.length; i++) {
const v = alphabet.indexOf(base32[i]);
if (v === -1) continue;
buf = (buf << 5) | v;
bufBits += 5;
if (bufBits >= 8) {
bufBits -= 8;
bytes[byteIdx++] = (buf >> bufBits) & 0xff;
}
}
return bytes;
}

async function generateTOTP(secret) {
const keyBytes = base32ToBytes(secret);
const counter = Math.floor(Date.now() / 1000 / 30);
const counterBuf = new ArrayBuffer(8);
new DataView(counterBuf).setBigUint64(0, BigInt(counter));

const cryptoKey = await crypto.subtle.importKey(
'raw', keyBytes, { name: 'HMAC', hash: 'SHA-1' }, false, ['sign']
);
const hmac = await crypto.subtle.sign('HMAC', cryptoKey, counterBuf);
const hmacBytes = new Uint8Array(hmac);
const offset = hmacBytes[19] & 0xf;
const code = ((hmacBytes[offset] & 0x7f) << 24 |
(hmacBytes[offset + 1] & 0xff) << 16 |
(hmacBytes[offset + 2] & 0xff) << 8 |
(hmacBytes[offset + 3] & 0xff)) % 1000000;
return code.toString().padStart(6, '0');
}

// ═══════════════════════════════════════════════════════════════════
// 3. QR Code Scanner ───────────────────────────────────────────────
// ═══════════════════════════════════════════════════════════════════
function decodeQRFromImage(img) {
if (!img.complete || img.naturalWidth === 0) return null;

const canvas = document.createElement('canvas');
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);

const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const result = jsQR(imageData.data, canvas.width, canvas.height);

if (result && result.data) {
return result.data;
}
return null;
}

function extractSecretFromOtpAuth(otpauthUrl) {
// otpauth://totp/...?secret=XXXX&...
const match = otpauthUrl.match(/[?&]secret=([A-Za-z2-7]+)/);
return match ? match[1] : null;
}

function isLikelyQRCode(img) {
// Heuristic: QR codes are usually square images, 150-300px
const w = img.naturalWidth;
const h = img.naturalHeight;
if (w < 100 || h < 100 || w > 600 || h > 600) return false;
const ratio = Math.max(w, h) / Math.min(w, h);
return ratio < 1.3;
}

function scanForQRCodes() {
const imgs = document.querySelectorAll('img');
let found = false;
for (const img of imgs) {
if (!isLikelyQRCode(img)) continue;
const data = decodeQRFromImage(img);
if (data && data.startsWith('otpauth://')) {
const secret = extractSecretFromOtpAuth(data);
if (secret) {
saveSecret(secret);
found = true;
break;
}
}
}
return found;
}

function decodeQRFromBlob(blob) {
return new Promise(function (resolve) {
const img = new Image();
img.onload = function () {
const canvas = document.createElement('canvas');
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const result = jsQR(imageData.data, canvas.width, canvas.height);
URL.revokeObjectURL(img.src);
resolve(result ? result.data : null);
};
img.onerror = function () {
URL.revokeObjectURL(img.src);
resolve(null);
};
img.src = URL.createObjectURL(blob);
});
}

function showPasteDialog(onResult) {
const overlay = document.createElement('div');
overlay.style.cssText = 'position:fixed;inset:0;z-index:100001;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;';

const dialog = document.createElement('div');
dialog.style.cssText = 'background:#fff;border-radius:12px;padding:32px;text-align:center;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;box-shadow:0 8px 32px rgba(0,0,0,.3);min-width:320px;';
dialog.innerHTML = '<div style="font-size:16px;font-weight:600;margin-bottom:12px;">粘贴二维码图片</div>' +
'<div style="font-size:13px;color:#666;margin-bottom:16px;">先截图/复制二维码,然后在下方按 Ctrl+V</div>' +
'<div id="ali-mfa-paste-zone" contenteditable="true" style="border:2px dashed #ccc;border-radius:8px;min-height:120px;display:flex;align-items:center;justify-content:center;color:#999;font-size:14px;outline:none;cursor:text;">点击此处后按 Ctrl+V 粘贴</div>' +
'<div style="margin-top:16px;"><button id="ali-mfa-paste-close" style="padding:6px 20px;border:1px solid #ddd;border-radius:6px;background:#f5f5f5;cursor:pointer;font-size:13px;">关闭</button></div>';

overlay.appendChild(dialog);
document.body.appendChild(overlay);

const zone = document.getElementById('ali-mfa-paste-zone');
const closeBtn = document.getElementById('ali-mfa-paste-close');

zone.focus();

function cleanup() { overlay.remove(); }

closeBtn.addEventListener('click', function () { cleanup(); onResult(false); });
overlay.addEventListener('click', function (e) { if (e.target === overlay) { cleanup(); onResult(false); } });

zone.addEventListener('paste', async function (e) {
e.preventDefault();
const files = e.clipboardData && e.clipboardData.files;
if (!files || files.length === 0) {
zone.textContent = '未检测到图片,请重试';
return;
}
const file = Array.from(files).find(f => f.type.startsWith('image/'));
if (!file) {
zone.textContent = '剪贴板中没有图片';
return;
}
zone.textContent = '识别中...';
const data = await decodeQRFromBlob(file);
if (data && data.startsWith('otpauth://')) {
const secret = extractSecretFromOtpAuth(data);
if (secret) {
saveSecret(secret);
cleanup();
onResult(true);
return;
}
}
if (data) {
zone.textContent = '二维码非OTP格式: ' + data.substring(0, 60);
} else {
zone.textContent = '未识别到二维码,请确认图片清晰完整后重试';
}
});
}

// ═══════════════════════════════════════════════════════════════════
// 4. Secret Storage ────────────────────────────────────────────────
// ═══════════════════════════════════════════════════════════════════
function getSecret() {
return GM_getValue(STORAGE_KEY, null);
}

function saveSecret(secret) {
GM_setValue(STORAGE_KEY, secret);
showToast('MFA密钥已保存 (' + domainKey + ')');
}

function deleteSecret() {
GM_deleteValue(STORAGE_KEY);
showToast('MFA密钥已删除 (' + domainKey + ')');
}

// ═══════════════════════════════════════════════════════════════════
// 5. UI Components ─────────────────────────────────────────────────
// ═══════════════════════════════════════════════════════════════════
function showToast(msg) {
const existing = document.querySelector('.ali-mfa-toast');
if (existing) existing.remove();

const toast = document.createElement('div');
toast.className = 'ali-mfa-toast';
toast.textContent = msg;
toast.style.cssText = 'position:fixed;top:16px;left:50%;transform:translateX(-50%);z-index:100000;padding:10px 24px;background:#1a1a1a;color:#fff;font-size:14px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;border-radius:8px;box-shadow:0 4px 16px rgba(0,0,0,.25);pointer-events:none;transition:opacity 0.4s;';
document.body.appendChild(toast);

setTimeout(function () {
toast.style.opacity = '0';
setTimeout(function () { toast.remove(); }, 400);
}, 2500);
}

function injectButtonStyle() {
if (document.getElementById('ali-mfa-btn-style')) return;
const style = document.createElement('style');
style.id = 'ali-mfa-btn-style';
style.textContent = [
'.ali-mfa-fill-btn {',
' display:inline-flex;align-items:center;justify-content:center;',
' margin-left:8px;padding:0 12px;height:32px;',
' background:#ff6a00;color:#fff;border:none;border-radius:4px;',
' font-size:13px;cursor:pointer;white-space:nowrap;',
' transition:background 0.15s;',
'}',
'.ali-mfa-fill-btn:hover { background:#e85d00; }',
'.ali-mfa-fill-btn.has-secret { background:#0077cc; }',
'.ali-mfa-fill-btn.has-secret:hover { background:#0066b3; }',
'.ali-mfa-actions {',
' display:inline-flex;align-items:center;margin-left:8px;gap:6px;',
'}',
].join('\n');
document.head.appendChild(style);
}

function findMFAInputs() {
const candidates = [];
const inputs = document.querySelectorAll('input[type="text"]:not([readonly]):not([disabled]), input[type="number"]:not([readonly]):not([disabled]), input:not([type]):not([readonly]):not([disabled])');

for (const inp of inputs) {
// Check various MFA indicators
const attrs = (inp.outerHTML + ' ' +
(inp.placeholder || '') + ' ' +
(inp.name || '') + ' ' +
(inp.id || '') + ' ' +
(inp.getAttribute('aria-label') || '')).toLowerCase();

const mfaKeywords = ['mfa', 'totp', 'code', 'captcha', 'otp', 'verify', 'verification', 'token', 'passcode', 'auth', '验证', '认证', '动态', '口令', '两步', '安全码'];

const hasMFASignal = mfaKeywords.some(function (kw) { return attrs.indexOf(kw) !== -1; });
const isSixDigitField = (inp.getAttribute('maxlength') === '6' || inp.getAttribute('size') === '6');

if (hasMFASignal || isSixDigitField) {
candidates.push(inp);
}
}
return candidates;
}

function createFillButton(inputEl, secret) {
const parent = inputEl.parentElement;
if (!parent) return null;

// Avoid duplicate buttons
const existing = parent.querySelector('.ali-mfa-actions');
if (existing) return null;

const wrapper = document.createElement('span');
wrapper.className = 'ali-mfa-actions';

const fillBtn = document.createElement('button');
fillBtn.className = 'ali-mfa-fill-btn';
fillBtn.textContent = '复制MFA';
fillBtn.title = '生成验证码并复制到剪贴板,然后粘贴到输入框';

fillBtn.addEventListener('click', async function () {
fillBtn.textContent = '...';
fillBtn.disabled = true;
try {
const code = await generateTOTP(secret);
await navigator.clipboard.writeText(code);
fillBtn.textContent = '已复制 ' + code;
showToast('验证码 ' + code + ' 已复制,请粘贴到输入框');
setTimeout(function () { fillBtn.textContent = '复制MFA'; }, 3000);
} catch (e) {
fillBtn.textContent = '失败';
setTimeout(function () { fillBtn.textContent = '复制MFA'; }, 2000);
}
fillBtn.disabled = false;
});

wrapper.appendChild(fillBtn);
parent.appendChild(wrapper);
return wrapper;
}

// ═══════════════════════════════════════════════════════════════════
// 6. Main MFA Watcher ──────────────────────────────────────────────
// ═══════════════════════════════════════════════════════════════════
function processMFA() {
// Scan QR codes
scanForQRCodes();

// Try to add fill buttons to MFA inputs
const secret = getSecret();
if (!secret) return;

const inputs = findMFAInputs();
for (const inp of inputs) {
createFillButton(inp, secret);
}
}

function initMFA() {
injectButtonStyle();

// Initial scan
processMFA();

// Watch DOM changes for dynamically loaded MFA components
const observer = new MutationObserver(function () {
processMFA();
});
observer.observe(document.body, { childList: true, subtree: true });

// Also retry periodically in case images load after DOM insertion
setInterval(processMFA, 2000);
}

// ═══════════════════════════════════════════════════════════════════
// 7. Secret Management Panel ───────────────────────────────────────
// ═══════════════════════════════════════════════════════════════════
function initMFAIndicator() {
const secret = getSecret();
const indicator = document.createElement('div');
indicator.style.cssText = 'position:fixed;left:12px;bottom:120px;z-index:99998;padding:6px 12px;background:' + (secret ? 'rgba(0,136,68,0.85)' : 'rgba(136,136,136,0.75)') + ';color:#fff;font-size:12px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;border-radius:6px;cursor:pointer;box-shadow:0 2px 8px rgba(0,0,0,.15);transition:background 0.2s;user-select:none;white-space:nowrap;';
indicator.textContent = secret ? 'MFA: ' + domainKey + ' ✓' : 'MFA: 未设置';

indicator.addEventListener('click', function () {
const currentSecret = getSecret();
if (currentSecret) {
if (confirm('删除 ' + domainKey + ' 的MFA密钥?\n\n当前密钥: ' + currentSecret)) {
deleteSecret();
indicator.textContent = 'MFA: 未设置';
indicator.style.background = 'rgba(136,136,136,0.75)';
}
} else {
var input = prompt('请粘贴 ' + domainKey + ' 的TOTP密钥或 otpauth:// 链接:\n\n可从 Authenticator App 导出二维码内容,或从初始绑定页面获取');
if (input) {
input = input.replace(/\s+/g, '');

// Support otpauth:// URL — auto-extract secret
if (input.startsWith('otpauth://')) {
var secretMatch = input.match(/[?&]secret=([A-Za-z2-7]+)/);
if (secretMatch) {
input = secretMatch[1].toUpperCase();
} else {
showToast('无法从链接中提取密钥,请检查链接是否完整');
return;
}
} else {
input = input.toUpperCase();
}

if (/^[A-Z2-7]+=*$/.test(input) && input.length >= 16) {
saveSecret(input);
indicator.textContent = 'MFA: ' + domainKey + ' ✓';
indicator.style.background = 'rgba(0,136,68,0.85)';
} else {
showToast('密钥格式无效,请输入有效的Base32密钥');
}
}
}
});

document.body.appendChild(indicator);

// Clipboard QR scan button
const clipBtn = document.createElement('div');
clipBtn.textContent = '剪贴板识别';
clipBtn.style.cssText = 'position:fixed;left:12px;bottom:80px;z-index:99998;padding:6px 12px;background:rgba(0,119,204,0.85);color:#fff;font-size:12px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;border-radius:6px;cursor:pointer;box-shadow:0 2px 8px rgba(0,0,0,.15);transition:background 0.2s;user-select:none;white-space:nowrap;';
clipBtn.addEventListener('mouseenter', function () { this.style.background = 'rgba(0,119,204,1)'; });
clipBtn.addEventListener('mouseleave', function () { this.style.background = 'rgba(0,119,204,0.85)'; });
clipBtn.addEventListener('click', function () {
showPasteDialog(function (ok) {
if (ok) {
indicator.textContent = 'MFA: ' + domainKey + ' ✓';
indicator.style.background = 'rgba(0,136,68,0.85)';
}
});
});
document.body.appendChild(clipBtn);
}

// ═══════════════════════════════════════════════════════════════════
// 8. Init ──────────────────────────────────────────────────────────
// ═══════════════════════════════════════════════════════════════════
initSiteSwitcher();
initMFA();
initMFAIndicator();
})();