Last active
January 8, 2026 01:46
-
-
Save PrintNow/03829be994c2866de0ed7963b94e41d2 to your computer and use it in GitHub Desktop.
AWS 自动登录脚本 (包括自动填充 OTOP)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name AWS 自动登录脚本 | |
| // @namespace https://nowtime.cc | |
| // @version v1.5.0 | |
| // @description 自动登录 AWS 控制台, 2026-01-08 更新:新增支持更新账号密码 | |
| // @author Shine | |
| // @match https://signin.aws.amazon.com/oauth?* | |
| // @match https://*.signin.aws.amazon.com/oauth?* | |
| // @icon https://www.google.com/s2/favicons?sz=64&domain=amazon.com | |
| // @grant GM.setValue | |
| // @grant GM.getValue | |
| // @grant GM.deleteValue | |
| // @run-at document-idle | |
| // ==/UserScript== | |
| (async function () { | |
| 'use strict'; | |
| const context = { | |
| otopConfig: {}, | |
| username: undefined, | |
| password: undefined, | |
| } | |
| // 初始化数据 | |
| window.addEventListener('load', await initData(() => { | |
| setTimeout(() => { | |
| console.log('开始执行自动登录 AWS') | |
| addResetLinks() | |
| handleToLogin() | |
| }, 800) | |
| })); | |
| async function initData(then) { | |
| let otopURL = await GM.getValue('otopURL'), | |
| username = await GM.getValue('username'), | |
| password = await GM.getValue('password'); | |
| if (!otopURL) { | |
| otopURL = prompt("请输入 OTPAuth URL") | |
| if (otopURL === '') { | |
| throw new Error(`请输入 OTPAuth URL`) | |
| } | |
| GM.setValue('otopURL', otopURL); | |
| console.log('otopURL 设置成功') | |
| } | |
| if (!username) { | |
| username = prompt("请输入 AWS 用户名") | |
| if (username === '') { | |
| throw new Error(`请输入用户名`) | |
| } | |
| GM.setValue('username', username); | |
| console.log('AWS 用户名设置成功') | |
| } | |
| if (!password) { | |
| password = prompt("请输入 AWS 密码") | |
| if (password === '') { | |
| throw new Error(`请输入密码`) | |
| } | |
| GM.setValue('password', password); | |
| console.log('AWS 密码设置成功') | |
| } | |
| context.username = username; | |
| context.password = password; | |
| context.otopConfig = parseOtpauthURL(otopURL); | |
| then && then() | |
| } | |
| /** | |
| * | |
| * @param attempt 重试次数 | |
| * @param action 0:输入账号密码页面,1:输入 OTP 页面 | |
| */ | |
| async function handleToLogin(attempt = 0, action = 0) { | |
| // 如果实在输入账号密码页面 | |
| if (action === 0) { | |
| const $signin_button = document.getElementById(`signin_button`) | |
| if (!$signin_button) { | |
| if (attempt > 10) { | |
| const msg = `[ACTION:${action}] 已尝试 ${attempt} 次 (per 200ms),未找到 #signin_button 元素` | |
| alert(msg) | |
| throw new Error(msg) | |
| } | |
| setTimeout(() => { | |
| handleToLogin(attempt + 1) | |
| }, 200) | |
| return; | |
| } | |
| setInputValue(document.querySelector(`input[name="username"]`), context.username) | |
| setInputValue(document.querySelector(`input[name="password"]`), context.password) | |
| // 模拟点击按钮 | |
| $signin_button.click(); | |
| setTimeout(() => { | |
| handleToLogin(0, 1) | |
| }, 200) | |
| } else if (action === 1) { | |
| // 输入 OTP 页面 | |
| if (attempt > 10) { | |
| const msg = `[ACTION:${action}] 已尝试 ${attempt} 次 (per 200ms)` | |
| alert(msg) | |
| throw new Error(msg) | |
| } | |
| const $otp_input = document.querySelector(`input[name="mfaCode"]`) | |
| if (!$otp_input) { | |
| setTimeout(() => { | |
| handleToLogin(attempt + 1, 1) | |
| }, 200) | |
| return; | |
| } | |
| const otp = await generateTOTP(context.otopConfig.secret, { | |
| step: context.otopConfig.period, | |
| digits: context.otopConfig.digits, | |
| algorithm: context.otopConfig?.algorithm ?? 'SHA-1' | |
| }); | |
| setInputValue($otp_input, otp, ["input", "change"]); | |
| // 模拟点击 | |
| document.querySelector(`button[data-testid="mfa-submit-button"]`).click() | |
| } | |
| } | |
| function setInputValue(input, value, trigger = ["input"]) { | |
| const setter = Object.getOwnPropertyDescriptor( | |
| Object.getPrototypeOf(input), | |
| "value" | |
| )?.set; | |
| if (setter) { | |
| setter.call(input, value); // 最通用 | |
| } else { | |
| input.value = value; | |
| } | |
| trigger.forEach(eventName => { | |
| input.dispatchEvent(new Event(eventName, {bubbles: true})); | |
| }); | |
| } | |
| function base32Decode(input) { | |
| const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; | |
| const clean = input.replace(/=+$/, "").toUpperCase(); | |
| let bits = ""; | |
| for (let char of clean) { | |
| const val = alphabet.indexOf(char); | |
| if (val === -1) continue; | |
| bits += val.toString(2).padStart(5, "0"); | |
| } | |
| const bytes = []; | |
| for (let i = 0; i + 8 <= bits.length; i += 8) { | |
| bytes.push(parseInt(bits.slice(i, i + 8), 2)); | |
| } | |
| return new Uint8Array(bytes); | |
| } | |
| async function generateTOTP(secret, { | |
| step = 30, // Time step (default 30 seconds) | |
| digits = 6, // Output digits | |
| algorithm = "SHA-1" // SHA-1, SHA-256, SHA-512 | |
| } = {}) { | |
| const keyBytes = base32Decode(secret); | |
| const counter = Math.floor(Date.now() / 1000 / step); | |
| // Convert counter to 8-byte buffer (Big Endian) | |
| const buf = new ArrayBuffer(8); | |
| const view = new DataView(buf); | |
| view.setUint32(4, counter); | |
| const algorithmMapping = { | |
| "SHA1": "SHA-1", | |
| "SHA256": "SHA-256", | |
| "SHA512": "SHA-512" | |
| }; | |
| algorithm = algorithm.toUpperCase();// 转换为大写 | |
| // 重新映射 | |
| if (Object.keys(algorithmMapping).includes(algorithm)) { | |
| algorithm = algorithmMapping[algorithm]; | |
| console.log('Re mapping', algorithm); | |
| } | |
| // 再次判断 | |
| if (!["SHA-1", "SHA-256", "SHA512"].includes(algorithm)) { | |
| let message = `algorithm: ${algorithm} 不符合预期`; | |
| alert(message) | |
| throw new Error(message) | |
| } | |
| // Import key for HMAC | |
| const cryptoKey = await crypto.subtle.importKey( | |
| "raw", | |
| keyBytes, | |
| {name: "HMAC", hash: algorithm}, | |
| false, | |
| ["sign"] | |
| ); | |
| const hmac = new Uint8Array( | |
| await crypto.subtle.sign("HMAC", cryptoKey, buf) | |
| ); | |
| // Dynamic Truncation | |
| const offset = hmac[hmac.length - 1] & 0x0f; | |
| const binary = | |
| ((hmac[offset] & 0x7f) << 24) | | |
| ((hmac[offset + 1] & 0xff) << 16) | | |
| ((hmac[offset + 2] & 0xff) << 8) | | |
| (hmac[offset + 3] & 0xff); | |
| const otp = (binary % 10 ** digits).toString().padStart(digits, "0"); | |
| return otp; | |
| } | |
| function parseOtpauthURL(url) { | |
| const u = new URL(url); | |
| if (u.protocol !== "otpauth:") { | |
| throw new Error("Invalid otpauth URL"); | |
| } | |
| const type = u.hostname; // totp / hotp | |
| const label = decodeURIComponent(u.pathname.slice(1)); | |
| const params = Object.fromEntries(u.searchParams.entries()); | |
| return { | |
| type, | |
| label, | |
| secret: params.secret, | |
| issuer: params.issuer, | |
| algorithm: params.algorithm || "SHA-1", | |
| digits: params.digits ? Number(params.digits) : 6, | |
| period: params.period ? Number(params.period) : 30, | |
| }; | |
| } | |
| /** | |
| * 查找并添加重置链接到 label 元素 | |
| * 只有在检测到登录错误元素时才添加重置链接 | |
| */ | |
| function addResetLinks() { | |
| const findAndAddLink = (labelId, resetFunction, linkText) => { | |
| const labelElement = document.getElementById(labelId); | |
| if (labelElement && !labelElement.querySelector('.aws-reset-link')) { | |
| const resetLink = document.createElement('a'); | |
| resetLink.href = 'javascript:void(0)'; | |
| resetLink.className = 'aws-reset-link'; | |
| resetLink.textContent = linkText; | |
| resetLink.style.cssText = 'margin-left: 8px; color: #0073bb; text-decoration: none; font-size: 12px;'; | |
| resetLink.addEventListener('click', (e) => { | |
| e.preventDefault(); | |
| resetFunction(); | |
| }); | |
| labelElement.appendChild(resetLink); | |
| } | |
| }; | |
| // 尝试查找元素,如果不存在则重试 | |
| let attempt = 0; | |
| const tryAddLinks = () => { | |
| // 先检测登录错误元素是否存在 | |
| const errorElement = document.querySelector('div[data-testid="iam-login-error"]'); | |
| // 只有检测到错误元素时才添加重置链接 | |
| if (errorElement) { | |
| const usernameLabel = document.getElementById('username-label'); | |
| const passwordLabel = document.getElementById('password-label'); | |
| if (usernameLabel) { | |
| findAndAddLink('username-label', resetUsername, '[重置]'); | |
| } | |
| if (passwordLabel) { | |
| findAndAddLink('password-label', resetPassword, '[重置]'); | |
| } | |
| // 如果两个元素都找到了,停止重试 | |
| if (usernameLabel && passwordLabel) { | |
| return; | |
| } | |
| } | |
| // 如果尝试次数超过 100 次,停止重试 | |
| if (attempt >= 100) { | |
| return; | |
| } | |
| attempt++; | |
| setTimeout(tryAddLinks, 500); | |
| }; | |
| tryAddLinks(); | |
| } | |
| /** | |
| * 重置用户名 | |
| */ | |
| async function resetUsername() { | |
| await GM.deleteValue('username'); | |
| const newUsername = prompt("请输入新的 AWS 用户名"); | |
| if (newUsername && newUsername !== '') { | |
| await GM.setValue('username', newUsername); | |
| context.username = newUsername; | |
| console.log('AWS 用户名已重置,刷新页面触发自动登录!'); | |
| // 更新输入框的值 | |
| const usernameInput = document.querySelector('input[name="username"]'); | |
| if (usernameInput) { | |
| setInputValue(usernameInput, newUsername); | |
| } | |
| } | |
| } | |
| /** | |
| * 重置密码 | |
| */ | |
| async function resetPassword() { | |
| await GM.deleteValue('password'); | |
| const newPassword = prompt("请输入新的 AWS 密码"); | |
| if (newPassword && newPassword !== '') { | |
| await GM.setValue('password', newPassword); | |
| context.password = newPassword; | |
| console.log('AWS 密码已重置,刷新页面触发自动登录!'); | |
| // 更新输入框的值 | |
| const passwordInput = document.querySelector('input[name="password"]'); | |
| if (passwordInput) { | |
| setInputValue(passwordInput, newPassword); | |
| } | |
| } | |
| } | |
| })(); |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
A. 安装前提
安装 Tampermonkey:https://chromewebstore.google.com/detail/tampermonkey/dhdgffkkebhmkfjojejmpbldmpobfkfo?hl=en
打开扩展开发者模式 - 访问

chrome://extensions/配置插件 Tampermonkey - 允许用户脚本

重启浏览器 - 输入

chrome://restart按回车B. 正式安装插件
“OTPAuth URL” 是什么?
AWS 添加 OTP 时,会给你一个二维码,使用相关软件扫描添加,你需要把这张二维码图片保存下来,使用二维码解析工具解析得到 URL,这个就是
OTPAuth URL格式类似于:
otpauth://totp/Amazon%20Web%20Services:shine@123456789?secret=SECRET-SECRET-SECRET-SECRET&issuer=Amazon%20Web%20Services