完成网络音乐、工具页面与安装器体验升级

This commit is contained in:
2026-08-18 13:54:33 +08:00
parent 337390f53e
commit 149c28082d
1082 changed files with 26442 additions and 162838 deletions
@@ -0,0 +1 @@
audio_match_demo
+429
View File
@@ -0,0 +1,429 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>听歌识曲</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #e0ecff 0%, #f5f0ff 50%, #fce4ec 100%);
}
.card {
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.6);
border-radius: 20px;
padding: 48px 40px;
width: 100%;
max-width: 420px;
box-shadow: 0 8px 32px rgba(100, 120, 180, 0.12), 0 2px 8px rgba(0, 0, 0, 0.04);
}
.card-header {
text-align: center;
margin-bottom: 36px;
}
.card-header h1 {
font-size: 26px;
font-weight: 600;
color: #1a1a2e;
letter-spacing: 0.5px;
}
.card-header p {
font-size: 14px;
color: #7c8db5;
margin-top: 8px;
}
.record-btn {
width: 100%;
padding: 14px;
font-size: 15px;
font-weight: 600;
color: #fff;
background: linear-gradient(135deg, #6c8cff 0%, #a78bfa 100%);
border: none;
border-radius: 12px;
cursor: pointer;
transition: all 0.3s ease;
letter-spacing: 0.5px;
}
.record-btn:hover {
transform: translateY(-1px);
box-shadow: 0 6px 20px rgba(108, 140, 255, 0.35);
}
.record-btn:active {
transform: translateY(0);
}
.record-btn.recording {
background: linear-gradient(135deg, #f87171 0%, #fb923c 100%);
animation: pulse 1.2s ease-in-out infinite;
}
.record-btn.system {
background: linear-gradient(135deg, #34d399 0%, #6c8cff 100%);
margin-top: 12px;
}
.record-btn.system.recording {
background: linear-gradient(135deg, #f87171 0%, #fb923c 100%);
}
@keyframes pulse {
0%, 100% { box-shadow: 0 0 0 0 rgba(248, 113, 113, 0.4); }
50% { box-shadow: 0 0 0 10px rgba(248, 113, 113, 0); }
}
.status {
text-align: center;
margin-top: 20px;
font-size: 14px;
color: #7c8db5;
min-height: 22px;
}
.player-wrap {
margin-top: 20px;
display: none;
}
.player-wrap audio {
width: 100%;
border-radius: 8px;
}
.result-section {
margin-top: 24px;
display: none;
}
.result-title {
font-size: 13px;
font-weight: 500;
color: #4a5568;
margin-bottom: 12px;
}
.result-card {
background: #f7f9fc;
border: 1.5px solid #e2e8f0;
border-radius: 12px;
padding: 16px;
}
.result-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 0;
border-bottom: 1px solid #edf2f7;
}
.result-item:last-child {
border-bottom: none;
}
.result-song {
font-size: 15px;
font-weight: 500;
color: #1a1a2e;
}
.result-artist {
font-size: 13px;
color: #7c8db5;
margin-top: 2px;
}
.result-score {
font-size: 13px;
font-weight: 600;
color: #6c8cff;
white-space: nowrap;
margin-left: 12px;
}
.no-result {
text-align: center;
color: #a0aec0;
font-size: 14px;
padding: 12px 0;
}
.back-link {
display: inline-flex;
align-items: center;
gap: 6px;
margin-top: 24px;
font-size: 14px;
color: #7c8db5;
text-decoration: none;
transition: color 0.25s ease;
}
.back-link:hover {
color: #6c8cff;
}
</style>
</head>
<body>
<div class="card">
<div class="card-header">
<h1>听歌识曲</h1>
<p>录制环境音频,识别歌曲信息</p>
</div>
<button class="record-btn" id="start">麦克风录制</button>
<button class="record-btn system" id="startSystem">系统内录</button>
<div class="status" id="status"></div>
<div class="player-wrap" id="playerWrap">
<audio id="player" controls></audio>
</div>
<div class="result-section" id="resultSection">
<div class="result-title">识别结果</div>
<div class="result-card" id="resultCard"></div>
</div>
<a class="back-link" href="./index.html">← 返回首页</a>
</div>
<script>
const startBtn = document.getElementById('start');
const startSystemBtn = document.getElementById('startSystem');
const statusEl = document.getElementById('status');
const playerWrap = document.getElementById('playerWrap');
const player = document.getElementById('player');
const resultSection = document.getElementById('resultSection');
const resultCard = document.getElementById('resultCard');
const SAMPLE_RATE = 8000;
const MAX_SECONDS = 10;
const CHANNELS = 1;
let audioChunks = [];
let mediaRecorder;
let activeStream;
let recordingSeconds = 0;
let recordingTimer;
const audioBufferToPCM = (audioBuffer) => {
const float32 = audioBuffer.getChannelData(0);
const int16 = new Int16Array(float32.length);
for (let i = 0; i < float32.length; i++) {
const s = Math.max(-1, Math.min(1, float32[i]));
int16[i] = s < 0 ? s * 0x8000 : s * 0x7fff;
}
return int16.buffer;
};
const decodeAudioBlob = async (blob) => {
const offlineCtx = new OfflineAudioContext(CHANNELS, SAMPLE_RATE * MAX_SECONDS, SAMPLE_RATE);
const arrayBuffer = await blob.arrayBuffer();
return await offlineCtx.decodeAudioData(arrayBuffer);
};
const getSystemAudioStream = async () => {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: false });
if (!stream || stream.getAudioTracks().length === 0) throw new Error('未获取到音频轨道');
return stream;
};
const getDisplayMediaStream = async () => {
const displayStream = await navigator.mediaDevices.getDisplayMedia({ audio: true, video: true });
const audioTracks = displayStream.getAudioTracks();
if (audioTracks.length === 0) {
displayStream.getTracks().forEach((t) => t.stop());
throw new Error('未获取到系统音频,请勾选"分享系统音频"');
}
displayStream.getVideoTracks().forEach((t) => t.stop());
return new MediaStream(audioTracks);
};
const recognizeAudio = (stream, btn) => {
audioChunks = [];
mediaRecorder = new MediaRecorder(stream, { mimeType: 'audio/webm;codecs=opus' });
mediaRecorder.addEventListener('dataavailable', (e) => {
if (e.data.size > 0) audioChunks.push(e.data);
});
mediaRecorder.addEventListener('stop', async (e) => {
if (audioChunks.length === 0) {
statusEl.textContent = '未录制到音频';
resetBtn();
setButtonsDisabled(false);
return;
}
statusEl.textContent = '正在识别...';
try {
const blob = new Blob(audioChunks, { type: 'audio/webm' });
playerWrap.style.display = 'block';
player.src = URL.createObjectURL(blob);
const audioBuffer = await decodeAudioBlob(blob);
const pcmData = audioBufferToPCM(audioBuffer);
const res = await fetch(`/audio/match?t=${Date.now()}`, {
method: 'POST',
headers: { 'Content-Type': 'application/octet-stream' },
body: pcmData,
});
const json = await res.json();
renderResult(json);
statusEl.textContent = '识别完成';
} catch (e) {
console.error('recognizeAudio', e);
statusEl.textContent = '识别失败,请重试';
}
resetBtn();
setButtonsDisabled(false);
});
mediaRecorder.start(500);
recordingTimer = setInterval(() => {
recordingSeconds += 1;
const left = MAX_SECONDS - recordingSeconds;
statusEl.textContent = left > 0 ? `录制中... ${left}s` : '正在处理...';
if (recordingSeconds >= MAX_SECONDS) stopRecording();
}, 1000);
};
function stopRecording() {
if (recordingTimer) {
clearInterval(recordingTimer);
recordingTimer = null;
}
if (mediaRecorder && mediaRecorder.state !== 'inactive') mediaRecorder.stop();
if (activeStream) {
activeStream.getTracks().forEach((t) => t.stop());
activeStream = null;
}
}
function cleanup() {
stopRecording();
if (player.src) {
URL.revokeObjectURL(player.src);
player.src = '';
}
}
function resetBtn() {
startBtn.textContent = '麦克风录制';
startBtn.classList.remove('recording');
startSystemBtn.textContent = '系统内录';
startSystemBtn.classList.remove('recording');
}
function renderResult(json) {
resultSection.style.display = 'block';
if (json.status !== 1 || !Array.isArray(json.data) || json.data.length === 0) {
resultCard.innerHTML = '<div class="no-result">未识别到歌曲</div>';
return;
}
const songs = json.data;
const timeStr = json.server_time ? ` · ${json.server_time}ms` : '';
resultCard.innerHTML =
`<div style="font-size:12px;color:#a0aec0;margin-bottom:12px;">找到 ${songs.length} 个结果${timeStr}</div>` +
songs
.map((item) => {
const name = item.songname || '未知';
const suffix = item.songNameSuffix ? ` (${item.songNameSuffix})` : '';
const artist = item.singername || '未知歌手';
const album = item.album?.[0]?.albumname || '';
const dist = item.dist ? `${(parseFloat(item.dist) * 100).toFixed(0)}%` : '';
const cover = item.union_cover?.replace('{size}', '100') || '';
const duration = item.timelength_128 ? formatDuration(item.timelength_128) : '';
return `
<div class="result-item">
${cover ? `<img src="${cover}" style="width:48px;height:48px;border-radius:8px;object-fit:cover;margin-right:12px;" />` : ''}
<div style="flex:1;min-width:0;">
<div class="result-song">${name}${suffix}</div>
<div class="result-artist">${artist}${album ? ` · ${album}` : ''}</div>
</div>
<div style="text-align:right;margin-left:12px;">
${dist ? `<div class="result-score">${dist}</div>` : ''}
${duration ? `<div style="font-size:12px;color:#a0aec0;margin-top:2px;">${duration}</div>` : ''}
</div>
</div>
`;
})
.join('');
}
function formatDuration(ms) {
const s = Math.floor(ms / 1000);
const m = Math.floor(s / 60);
const sec = s % 60;
return `${m}:${String(sec).padStart(2, '0')}`;
}
function setButtonsDisabled(disabled) {
if (!startBtn.classList.contains('recording')) startBtn.disabled = disabled;
if (!startSystemBtn.classList.contains('recording')) startSystemBtn.disabled = disabled;
}
async function startRecordWith(getStream, btn) {
if (btn.classList.contains('recording')) {
btn.textContent = '处理中...';
btn.disabled = true;
stopRecording();
return;
}
cleanup();
audioChunks = [];
recordingSeconds = 0;
playerWrap.style.display = 'none';
resultSection.style.display = 'none';
statusEl.textContent = '准备录制...';
btn.textContent = '停止录制';
btn.classList.add('recording');
setButtonsDisabled(true);
try {
const stream = await getStream();
activeStream = stream;
recognizeAudio(stream, btn);
} catch (e) {
console.error(e);
statusEl.textContent = e.message || '获取音频失败';
resetBtn();
setButtonsDisabled(false);
}
}
startBtn.addEventListener('click', () => startRecordWith(getSystemAudioStream, startBtn));
startSystemBtn.addEventListener('click', () => startRecordWith(getDisplayMediaStream, startSystemBtn));
</script>
</body>
</html>
+365
View File
@@ -0,0 +1,365 @@
/**
* 浏览器端行为指纹生成工具
*
* 从 login_captcha_simulate.html 提取的独立模块,提供:
* - generateWebGLHash(): 生成 WebGL 指纹哈希值
* - generateEDTData(opts): 生成用户行为指纹数据(用于 sid/edt 加密的 data 字段)
*
* 适用于 login_captcha.html 和 login_captcha_simulate.html 共用
*/
(function (root) {
'use strict';
/**
* 生成 [min, max] 范围内的随机整数(包含两端)
* @param {number} min - 最小值
* @param {number} max - 最大值
* @returns {number} 随机整数
*/
function ri(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
// ============================================================
// 事件记录格式化函数
// ============================================================
/**
* 格式化 type-3 事件(鼠标/触摸移动)
* @param {number} t - 时间差(毫秒)
* @param {number} i - 子索引(0 或 1
* @param {number} x - 鼠标 X 坐标
* @param {number} y - 鼠标 Y 坐标
* @returns {string} 格式: "3,时间差,子索引,X,Y"
*/
function f3(t, i, x, y) {
return '3,' + t + ',' + i + ',' + x + ',' + y;
}
/**
* 格式化 type-5 事件(滚动/计时)
* @param {number} t - 时间差(毫秒)
* @param {number} i - 事件索引
* @returns {string} 格式: "5,时间差,事件索引"
*/
function f5(t, i) {
return '5,' + t + ',' + i;
}
/**
* 格式化 type-6 事件(窗口事件)
* @param {number} t - 时间差(毫秒)
* @param {number} i - 事件索引
* @param {number} x - 窗口宽度
* @param {number} y - 窗口高度
* @returns {string} 格式: "6,时间差,事件索引,宽,高"
*/
function f6(t, i, x, y) {
return '6,' + t + ',' + i + ',' + x + ',' + y;
}
/**
* 格式化 type-3 哨兵记录(鼠标事件结束标记)
* @param {number} sentinel - 哨兵值
* @param {number} i - 子索引
* @param {number} x - X 坐标
* @param {number} y - Y 坐标
* @returns {string} 格式: "3,SENTINEL,子索引,X,Y"
*/
function fs3(sentinel, i, x, y) {
return '3,' + sentinel + ',' + i + ',' + x + ',' + y;
}
/**
* 格式化 type-5 哨兵记录(滚动事件结束标记)
* @param {number} sentinel - 哨兵值
* @param {number} i - 事件索引
* @returns {string} 格式: "5,SENTINEL,事件索引"
*/
function fs5(sentinel, i) {
return '5,' + sentinel + ',' + i;
}
/**
* 格式化 type-6 哨兵记录(窗口事件结束标记)
* @param {number} sentinel - 哨兵值
* @param {number} i - 事件索引
* @param {number} x - 窗口宽度
* @param {number} y - 窗口高度
* @returns {string} 格式: "6,SENTINEL,事件索引,宽,高"
*/
function fs6(sentinel, i, x, y) {
return '6,' + sentinel + ',' + i + ',' + x + ',' + y;
}
// ============================================================
// 贝塞尔曲线鼠标路径生成
// ============================================================
/**
* 用三阶贝塞尔曲线生成模拟真人的鼠标移动路径
*
* 真人鼠标轨迹特点:
* - 不是直线,有弧度和加速减速
* - 有微小抖动(手抖)
* - 起步慢、中间快、结束减速
*
* @param {number} sx - 起点 X
* @param {number} sy - 起点 Y
* @param {number} ex - 终点 X
* @param {number} ey - 终点 Y
* @param {number} n - 采样点数
* @returns {Array<{x:number, y:number}>} 路径点数组
*/
function bezierPath(sx, sy, ex, ey, n) {
const c1x = sx + (ex - sx) * 0.3 + ri(-80, 80);
const c1y = sy + (ey - sy) * 0.2 + ri(-60, 60);
const c2x = sx + (ex - sx) * 0.7 + ri(-60, 60);
const c2y = sy + (ey - sy) * 0.8 + ri(-40, 40);
const pts = [];
for (let i = 0; i <= n; i++) {
const t = i / n;
const u = 1 - t;
const x = u * u * u * sx + 3 * u * u * t * c1x + 3 * u * t * t * c2x + t * t * t * ex;
const y = u * u * u * sy + 3 * u * u * t * c1y + 3 * u * t * t * c2y + t * t * t * ey;
const jitter = Math.max(0.5, 3 - t * 2.5);
pts.push({
x: x + (Math.random() - 0.5) * jitter,
y: y + (Math.random() - 0.5) * jitter,
});
}
return pts;
}
// ============================================================
// 二进制/hex/Base64 转换工具
// ============================================================
/**
* hex 字符串转 ArrayBuffer
* @param {string} hex - hex 字符串(如 "6b75676f"
* @returns {ArrayBuffer} 对应的二进制缓冲区
*/
function hex2buf(hex) {
const arr = new Uint8Array(hex.length / 2);
for (let i = 0; i < arr.length; i++) arr[i] = parseInt(hex.substr(i * 2, 2), 16);
return arr.buffer;
}
/**
* ArrayBuffer 转 hex 字符串
* @param {ArrayBuffer} buf - 二进制缓冲区
* @returns {string} hex 字符串(每个字节两位,小写)
*/
function buf2hex(buf) {
return Array.from(new Uint8Array(buf))
.map((b) => b.toString(16).padStart(2, '0'))
.join('');
}
/**
* hex 字符串转 Base64 字符串
* @param {string} hex - hex 字符串
* @returns {string} Base64 编码的字符串
*/
function hexToBase64(hex) {
const bytes = new Uint8Array(hex.length / 2);
for (let i = 0; i < bytes.length; i++) bytes[i] = parseInt(hex.substr(i * 2, 2), 16);
let binary = '';
for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
return btoa(binary);
}
// ============================================================
// 加密常量
// ============================================================
/**
* RSA 公钥(SPKI DER 格式的 hex 字符串)
* 从 WASM 二进制中提取,用于 RSA-OAEP SHA-256 加密 AES 密钥
* 算法: RSA-2048,公钥指数 65537 (0x10001)
*/
const RSA_SPKI_HEX =
'30820122300d06092a864886f70d01010105000382010f003082010a0282010100a16dbe625a3c00b78f4904cfd31045945984387bc10fdb52facec30657ca12edd1cf3bd94da5f526d61b5f8f80554aa3e80473f0833e08a072a8616f6c737f5bae17c4d23eabbcf7e9a8c22f75532765b91bd302262b5cea819b8ab7b83507e1684ab49c2fa1c41590bc26c815f940d88b6b2d46d253bcf56c703f6be8e5426e0e5af63e20a9d3af23894cfb93d7234e5636c9f3004b2b2d83810afda4fa963e6110b46a51e4833d57c29aa3a3da49d29839619b5f78b6f91cc82a1bd9531c6d2707556ea3e50cf956f61e3fc4805ce7a2e0bebe1a225f2716dc1b8f85095544c5b86aecd2d63d1ffb57bd9db675408ab86c56fe05bb645fa05f3eaf1ed61aad0203010001';
/**
* AES 初始化向量(固定值)
* ASCII 解码为 "kugousecurity123"
* WASM 中硬编码,每次加密都使用相同的 IV
*/
const AES_IV_HEX = '6b75676f757365637572697479313233';
// ============================================================
// 加密流程
// ============================================================
/**
* 完整的 sid 加密流程(纯 JS 实现,不依赖 WASM)
*
* 加密方案:
* 1. 生成随机 AES-128 密钥
* 2. 用 AES-128-CBC 加密行为指纹明文 → 得到 EDT
* 3. 用 RSA-OAEP SHA-256 加密 AES 密钥 → 得到 SID
* 4. 服务端用 RSA 私钥解密 SID 得到 AES 密钥,再用 AES 密钥解密 EDT 得到行为数据
*
* @param {string} plaintext - 待加密的明文(行为指纹数据)
* @returns {Promise<Object>} 包含明文、密钥、密文等所有中间数据
*/
async function encryptSid(plaintext) {
const aesKeyRaw = crypto.getRandomValues(new Uint8Array(16));
const aesKeyHex = buf2hex(aesKeyRaw);
const aesKey = await crypto.subtle.importKey('raw', aesKeyRaw, { name: 'AES-CBC' }, false, ['encrypt']);
const ptBuf = new TextEncoder().encode(plaintext);
const iv = new Uint8Array(hex2buf(AES_IV_HEX));
const aesCt = await crypto.subtle.encrypt({ name: 'AES-CBC', iv: iv }, aesKey, ptBuf);
const rsaKey = await crypto.subtle.importKey('spki', hex2buf(RSA_SPKI_HEX), { name: 'RSA-OAEP', hash: 'SHA-256' }, false, ['encrypt']);
const rsaCt = await crypto.subtle.encrypt({ name: 'RSA-OAEP' }, rsaKey, aesKeyRaw);
return {
plaintext,
aesKeyHex,
aesIvHex: AES_IV_HEX,
aesCiphertextHex: buf2hex(aesCt),
rsaCiphertextHex: buf2hex(rsaCt),
};
}
// ============================================================
// WebGL 指纹生成
// ============================================================
/**
* 生成 WebGL 指纹哈希值
*
* WebGL 指纹是浏览器指纹的重要组成部分,通过获取显卡厂商、渲染器名称、
* WebGL 版本和支持的扩展列表,生成一个唯一的哈希值。
*
* 浏览器环境:通过 canvas 真实渲染获取 WebGL 信息
* Node 环境或 WebGL 不可用时:生成随机 uint64 模拟值
*
* @returns {string} WebGL 指纹的十进制字符串表示
*/
function generateWebGLHash() {
if (typeof document !== 'undefined') {
try {
const canvas = document.createElement('canvas');
canvas.width = 200;
canvas.height = 50;
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
if (gl) {
const dbg = gl.getExtension('WEBGL_debug_renderer_info');
const vendor = dbg ? gl.getParameter(dbg.UNMASKED_VENDOR_WEBGL) : '';
const renderer = dbg ? gl.getParameter(dbg.UNMASKED_RENDERER_WEBGL) : '';
const version = gl.getParameter(gl.VERSION);
const exts = gl.getSupportedExtensions().join(',');
const s = vendor + '|' + renderer + '|' + version + '|' + exts;
let hash = BigInt('14695981039346656037');
const prime = BigInt('1099511628211');
for (let i = 0; i < s.length; i++) {
hash = hash ^ BigInt(s.charCodeAt(i));
hash = (hash * prime) & BigInt('0xFFFFFFFFFFFFFFFF');
}
return hash.toString();
}
} catch (e) {}
}
const hi = Math.floor(Math.random() * 0xffffffff);
const lo = Math.floor(Math.random() * 0xffffffff);
return (BigInt(hi) * BigInt(0x100000000) + BigInt(lo)).toString();
}
// ============================================================
// 行为数据生成
// ============================================================
/**
* 生成 sid 中的 data 字段(用户行为指纹数据)
*
* 模拟真实用户在页面上的交互行为,包括:
* - 窗口加载/resize 事件
* - 页面滚动事件
* - 鼠标移动轨迹
*
* 数据格式: type,value,index[,x,y] 各条目用 : 分隔
* 事件类型:
* 3 = 鼠标/触摸移动事件(带 x,y 坐标)
* 5 = 滚动/计时器事件
* 6 = 窗口事件(如 resize
*
* @param {Object} opts - 配置项
* @param {number} opts.startX - 鼠标起点 X 坐标
* @param {number} opts.startY - 鼠标起点 Y 坐标
* @param {number} opts.endX - 鼠标终点 X 坐标
* @param {number} opts.endY - 鼠标终点 Y 坐标
* @param {number} opts.mousePoints - 鼠标轨迹采样点数
* @returns {string} 编码后的 data 字段字符串
*/
function generateEDTData(opts) {
const { startX, startY, endX, endY, mousePoints } = opts;
const sentinel = 0xffffffff - Math.floor(Math.random() * 20);
const entries = [];
let ts = 0;
let ei = 0;
entries.push(f5(0, 0));
entries.push(fs5(sentinel, 0));
entries.push(f5(0, 0));
entries.push(fs5(sentinel, 0));
ts += ri(5, 20);
entries.push(f6(ts, ei, 750, 500));
entries.push(fs6(sentinel, ei, 750, 500));
ei++;
for (let i = 0; i < 3; i++) {
ts += ri(80, 600);
entries.push(f5(ts, ei));
entries.push(fs5(sentinel, ei));
ei++;
}
const path = bezierPath(startX, startY, endX, endY, mousePoints);
let si = 0;
for (let j = 0; j < path.length; j++) {
const p = path[j];
ts += ri(8, 50);
entries.push(f3(ts, si, Math.round(p.x), Math.round(p.y)));
entries.push(fs3(sentinel, si, Math.round(p.x), Math.round(p.y)));
if (j > 0 && j % 12 === 0) {
ts += ri(20, 60);
entries.push(f5(ts, ei));
entries.push(fs5(sentinel, ei));
ei++;
}
si = (si + 1) % 2;
}
ts += ri(5, 30);
entries.push(f3(ts, 1, Math.round(endX + ri(-5, 5)), Math.round(endY + ri(-5, 5))));
entries.push(fs3(sentinel, 1, Math.round(endX), Math.round(endY)));
return entries.join(':');
}
// ============================================================
// 导出
// ============================================================
const fingerprint = { generateWebGLHash, generateEDTData, encryptSid, hex2buf, buf2hex, hexToBase64, ri };
// 支持多种模块系统
if (typeof module !== 'undefined' && module.exports) {
module.exports = fingerprint;
} else {
root.fingerprint = fingerprint;
}
})(typeof window !== 'undefined' ? window : globalThis);
+171
View File
@@ -0,0 +1,171 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>酷狗音乐 API</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #e0ecff 0%, #f5f0ff 50%, #fce4ec 100%);
color: #1a1a2e;
}
.container {
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.6);
border-radius: 20px;
padding: 48px 40px;
width: 100%;
max-width: 480px;
text-align: center;
box-shadow: 0 8px 32px rgba(100, 120, 180, 0.12), 0 2px 8px rgba(0, 0, 0, 0.04);
}
.logo {
font-size: 42px;
margin-bottom: 8px;
}
h1 {
font-size: 28px;
font-weight: 600;
color: #1a1a2e;
margin-bottom: 12px;
}
.description {
font-size: 15px;
color: #7c8db5;
line-height: 1.6;
margin-bottom: 28px;
}
.doc-link {
display: inline-block;
padding: 10px 24px;
font-size: 14px;
font-weight: 500;
color: #fff;
background: linear-gradient(135deg, #6c8cff 0%, #a78bfa 100%);
border-radius: 10px;
text-decoration: none;
transition: all 0.3s ease;
margin-bottom: 32px;
}
.doc-link:hover {
transform: translateY(-1px);
box-shadow: 0 6px 20px rgba(108, 140, 255, 0.35);
}
h2 {
font-size: 17px;
font-weight: 600;
color: #4a5568;
margin-bottom: 16px;
}
.example-list {
list-style: none;
padding: 0;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 10px;
}
.example-list li {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
font-size: 15px;
color: #4a5568;
}
.example-list a {
color: #6c8cff;
text-decoration: none;
padding: 10px 20px;
background: #f7f9fc;
border: 1.5px solid #e2e8f0;
border-radius: 10px;
transition: all 0.25s ease;
width: 100%;
}
@media (max-width: 400px) {
.example-list { grid-template-columns: 1fr; }
}
.example-list a:hover {
border-color: #6c8cff;
background: #eef2ff;
color: #5a7ae6;
}
.example-list .index {
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
background: linear-gradient(135deg, #6c8cff 0%, #a78bfa 100%);
color: #fff;
font-size: 12px;
font-weight: 600;
border-radius: 6px;
flex-shrink: 0;
}
</style>
</head>
<body>
<div class="container">
<div class="logo">🎵</div>
<h1>酷狗音乐 API</h1>
<p class="description">当你看到这个页面时,这个服务已经成功跑起来了~</p>
<a class="doc-link" href="//kugoumusicapi-docs.4everland.app/">查看文档</a>
<h2>例子</h2>
<ul class="example-list">
<li>
<span class="index">1</span>
<a href="./search?keywords=海阔天空">搜索</a>
</li>
<li>
<span class="index">2</span>
<a href="./top/song">新歌速递</a>
</li>
<li>
<span class="index">3</span>
<a href="./everyday/recommend">每日推荐</a>
</li>
<li>
<span class="index">4</span>
<a href="./login_captcha.html">登录验证码</a>
</li>
<li>
<span class="index">5</span>
<a href="./verifySlide.html">验证码验证</a>
</li>
<li>
<span class="index">6</span>
<a href="./audio_match.html">听歌识曲</a>
</li>
</ul>
</div>
</body>
</html>
+420
View File
@@ -0,0 +1,420 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<!-- 移动端视口设置,确保页面在不同设备上正确缩放 -->
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>登录</title>
<!-- 引入验证码相关的 WASM/JS 工具包(提供 wasm_bindgen 等全局对象) -->
<script src="./verify-pkg/verifycode.js"></script>
<style>
/* ========== 全局重置样式 ========== */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* ========== 页面主体:全屏居中 + 渐变背景 ========== */
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #e0ecff 0%, #f5f0ff 50%, #fce4ec 100%);
}
/* ========== 登录卡片容器:毛玻璃效果 ========== */
.login-card {
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.6);
border-radius: 20px;
padding: 48px 40px;
width: 100%;
max-width: 400px;
box-shadow: 0 8px 32px rgba(100, 120, 180, 0.12), 0 2px 8px rgba(0, 0, 0, 0.04);
}
/* ========== 登录页头部标题区域 ========== */
.login-header {
text-align: center;
margin-bottom: 36px;
}
.login-header h1 {
font-size: 26px;
font-weight: 600;
color: #1a1a2e;
letter-spacing: 0.5px;
}
.login-header p {
font-size: 14px;
color: #7c8db5;
margin-top: 8px;
}
/* ========== 表单项通用样式 ========== */
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
font-size: 13px;
font-weight: 500;
color: #4a5568;
margin-bottom: 8px;
}
.form-group input {
width: 100%;
padding: 14px 16px;
font-size: 15px;
color: #1a1a2e;
background: #f7f9fc;
border: 1.5px solid #e2e8f0;
border-radius: 12px;
outline: none;
transition: all 0.25s ease;
}
/* 输入框占位符颜色 */
.form-group input::placeholder {
color: #a0aec0;
}
/* 输入框聚焦状态:蓝色边框 + 发光阴影 */
.form-group input:focus {
border-color: #6c8cff;
background: #fff;
box-shadow: 0 0 0 3px rgba(108, 140, 255, 0.12);
}
/* ========== 登录按钮 ========== */
.login-btn {
width: 100%;
padding: 14px;
font-size: 15px;
font-weight: 600;
color: #fff;
background: linear-gradient(135deg, #6c8cff 0%, #a78bfa 100%);
border: none;
border-radius: 12px;
cursor: pointer;
transition: all 0.3s ease;
margin-top: 8px;
letter-spacing: 0.5px;
}
/* 按钮悬停:上浮 + 阴影加深 */
.login-btn:hover {
transform: translateY(-1px);
box-shadow: 0 6px 20px rgba(108, 140, 255, 0.35);
}
/* 按钮点击:恢复原位 */
.login-btn:active {
transform: translateY(0);
}
/* ========== 腾讯验证码容器(默认隐藏,按需显示) ========== */
.captcha-box {
margin-top: 24px;
}
/* ========== 手机短信验证码区域(默认隐藏) ========== */
.sms-captcha {
margin-top: 24px;
display: none;
}
.sms-captcha .form-group {
margin-bottom: 16px;
}
/* 短信验证按钮样式(与登录按钮风格一致) */
.sms-captcha .verify-btn {
width: 100%;
padding: 14px;
font-size: 15px;
font-weight: 600;
color: #fff;
background: linear-gradient(135deg, #6c8cff 0%, #a78bfa 100%);
border: none;
border-radius: 12px;
cursor: pointer;
transition: all 0.3s ease;
margin-top: 8px;
}
.sms-captcha .verify-btn:hover {
transform: translateY(-1px);
box-shadow: 0 6px 20px rgba(108, 140, 255, 0.35);
}
/* ========== 返回首页链接 ========== */
.back-link {
display: inline-flex;
align-items: center;
gap: 6px;
margin-top: 20px;
font-size: 14px;
color: #7c8db5;
text-decoration: none;
transition: color 0.25s ease;
}
.back-link:hover {
color: #6c8cff;
}
</style>
</head>
<body>
<!-- ========== 登录卡片主容器 ========== -->
<div class="login-card">
<!-- 页面标题 -->
<div class="login-header">
<h1>欢迎登录</h1>
<p>请输入您的账号信息</p>
</div>
<!-- 用户名输入框 -->
<div class="form-group">
<label>用户名</label>
<input type="text" id="username" placeholder="请输入用户名" />
</div>
<!-- 密码输入框 -->
<div class="form-group">
<label>密码</label>
<input type="password" id="password" placeholder="请输入密码" />
</div>
<!-- 登录按钮 -->
<button class="login-btn" id="loginBtn">登 录</button>
<!-- 腾讯验证码挂载容器(由 JS 动态控制显示/隐藏) -->
<div class="captcha-box" id="captchaBox"></div>
<!-- 手机短信验证码输入区域(由 JS 动态控制显示/隐藏) -->
<div class="sms-captcha" id="smsCaptcha">
<div class="form-group">
<label>验证码</label>
<input type="text" id="smsCode" placeholder="请输入验证码" maxlength="6" />
</div>
<button class="verify-btn" id="smsVerifyBtn">验证</button>
</div>
<!-- 返回首页链接 -->
<a class="back-link" href="./index.html">← 返回首页</a>
</div>
<script>
/**
* ============================================================
* 登录页面主逻辑(WASM 版,依赖 verifycode.js
* ============================================================
*
* 与 login_captcha_simulate.html 的区别:
* - 本文件使用 WASM 二进制生成 sid/edt 加密参数
* - login_captcha_simulate.html 使用纯 JS 模拟,不依赖 WASM
*
* 整体流程:
* 1. 初始化 WASM 验证码加密模块(加载 verifycode_bg_ios.wasm
* 2. 用户输入账号密码,点击登录 → 调用 /login 接口
* 3. 若返回 error_code=20028,表示需要二次安全验证
* 4. 调用 /get/verify/info 获取验证详情(验证类型、腾讯验证码 appid 等)
* 5. 根据 v_type 判断验证类型:
* - v_type=23:腾讯图形验证码(滑块/点选)
* - v_type=32:手机短信验证码
* 6. 验证通过后调用 /verify/user/info 接口完成二次验证
*/
(async () => {
// ========== DOM 元素引用 ==========
const usernameInput = document.getElementById('username');
const passwordInput = document.getElementById('password');
const loginBtn = document.getElementById('loginBtn');
const captchaBox = document.getElementById('captchaBox'); // 腾讯验证码容器
const smsCaptcha = document.getElementById('smsCaptcha'); // 短信验证码区域
const smsCode = document.getElementById('smsCode'); // 短信验证码输入框
const smsVerifyBtn = document.getElementById('smsVerifyBtn'); // 短信验证按钮
/**
* 动态加载外部 JS 脚本
* @param {string} url - 脚本地址
* @param {function} cb - 加载完成/失败的回调函数
*/
function loadScript(url, cb) {
const s = document.createElement('script');
s.src = url;
s.onload = function () { cb(); };
s.onerror = function () { cb(new Error('load failed')); };
document.head.appendChild(s);
}
// ========== WASM 初始化 ==========
// wasm_bindgen 由 verify-pkg/verifycode.js 提供
// 用于生成加密参数 sid 和 edt,作为验证请求的安全令牌
if (typeof wasm_bindgen !== 'undefined' && wasm_bindgen.run) {
try {
await wasm_bindgen('./verify-pkg/verifycode_bg_ios.wasm');
wasm_bindgen.run();
} catch (e) {
console.warn('WASM initialization failed:', e);
}
}
// ========== 登录按钮点击事件 ==========
loginBtn.addEventListener('click', async () => {
const username = usernameInput.value;
const password = passwordInput.value;
// 使用 WASM 模块生成加密的会话标识
// sid: RSA-OAEP 加密的 AES 密钥(Session ID
// edt: AES-128-CBC 加密的行为数据(Encrypted Data Token
const eData = new wasm_bindgen.EData();
let sessionid = '';
try {
// 第一步:调用登录接口
const loginResult = await fetch(
`/login?username=${encodeURIComponent(username)}&password=${encodeURIComponent(password)}`
).then(response => response.json());
// status === 1 表示登录成功
if (loginResult.status === 1) {
alert('登录成功!');
} else {
// error_code === 20028 表示需要进行二次安全验证
// ssaCode 是服务端返回的验证事件 ID
if (loginResult.error_code === 20028 && loginResult.ssaCode) {
// 第二步:获取验证详情(验证类型、腾讯验证码 appid 等)
const verifyInfo = await fetch(
`/get/verify/info?eventid=${loginResult.ssaCode}`
).then(response => response.json());
// 优先使用服务端返回的 sid/edt,否则由 WASM 本地生成
const sid = loginResult?.sid || eData.get_sid();
const edt = loginResult?.edt || eData.get_edt();
console.log('sid:', sid, 'edt:', edt);
console.log('Verify Info:', verifyInfo);
sessionid = verifyInfo.data.sessionid; // 验证会话 ID
const txappid = verifyInfo.data.txappid; // 腾讯验证码应用 ID
const business = verifyInfo.data.business; // 业务标识
const v_type = verifyInfo.data.v_type; // 验证类型(23=腾讯验证码, 32=手机验证码)
if (v_type === 23) {
// ===== 腾讯图形验证码流程(v_type=23 =====
// 显示验证码容器,隐藏短信验证码区域
captchaBox.style.display = 'block';
smsCaptcha.style.display = 'none';
// 动态加载腾讯验证码 JS SDK
loadScript('https://turing.captcha.qcloud.com/TCaptcha.js', function () {
if (typeof TencentCaptcha === 'undefined') {
return;
}
const appId = txappid;
// 创建腾讯验证码实例
// 回调函数 res 包含验证结果:
// ret === 0 表示验证通过
// res.ticket: 验证票据
// res.randstr: 随机字符串(用于防重放)
const captcha = new TencentCaptcha(
appId,
function (res) {
console.log('Captcha response:', res);
if (res.ret === 0) {
// 验证通过:拼装验证数据并发送到服务端
// 格式:KGCodeTX|{ticket, randstr, txappid} 的 JSON 字符串
// KGCodeTX 是腾讯验证码的标识前缀
const verifycode = 'KGCodeTX|' + JSON.stringify({
ticket: res.ticket,
randstr: res.randstr,
txappid: appId,
});
// 第三步:将验证码结果提交到服务端进行校验
fetch(
`/verify/user/info?eventid=${loginResult.ssaCode}&v_type=${v_type}` +
`&verifycode=${encodeURIComponent(verifycode)}` +
`&sid=${encodeURIComponent(sid)}&edt=${encodeURIComponent(edt)}`,
{ method: 'GET' }
)
.then(response => response.json())
.then(data => {
console.log('Verification Result:', data);
})
.catch(error => {
console.error('Verification request failed:', error);
alert('验证码验证请求失败');
});
} else {
// 用户关闭了验证码弹窗或验证失败
alert(`用户取消 (ret=${res.ret})`);
}
},
{
type: '', // 验证码类型(空字符串表示使用后台配置的默认类型)
showHeader: false, // 不显示验证码弹窗标题栏
ready: function () {}, // 验证码就绪回调(当前无操作)
}
);
console.log('Captcha initialized:', captcha);
captcha.show(); // 弹出验证码窗口
});
} else if (v_type === 32) {
// ===== 手机短信验证码流程(v_type=32 =====
// 隐藏腾讯验证码容器,显示短信验证码输入区域
captchaBox.style.display = 'none';
smsCaptcha.style.display = 'block';
smsCode.value = '';
smsCode.focus(); // 自动聚焦到验证码输入框
// 绑定短信验证按钮的点击事件
smsVerifyBtn.onclick = async function () {
const code = smsCode.value.trim();
if (!code) {
alert('请输入验证码');
return;
}
try {
// 第三步:将手机验证码提交到服务端校验
const verifyResponse = await fetch(
`/verify/user/info?eventid=${loginResult.ssaCode}&v_type=${v_type}` +
`&verifycode=${encodeURIComponent(code)}` +
`&sid=${encodeURIComponent(sid)}&edt=${encodeURIComponent(edt)}`,
{ method: 'GET' }
);
const verifyData = await verifyResponse.json();
console.log('SMS Verification Result:', verifyData);
alert('验证成功');
} catch (error) {
console.error('SMS Verification request failed:', error);
alert('验证码验证请求失败');
}
};
} else {
// 未知的验证类型
console.warn('未知的验证类型:', v_type);
alert('未知的验证类型');
}
}
}
} catch (e) {
console.warn('Login request failed:', e);
}
});
})();
</script>
</body>
</html>
@@ -0,0 +1,459 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<!-- 移动端视口设置,确保页面在不同设备上正确缩放 -->
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>登录</title>
<style>
/* ========== 全局重置样式 ========== */
* {
margin: 0;
padding: 0;
box-sizing: border-box; /* 使用 border-box 模型,padding 不会撑大元素 */
}
/* ========== 页面主体:全屏居中 + 渐变背景 ========== */
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
/* 蓝紫粉三色渐变背景 */
background: linear-gradient(135deg, #e0ecff 0%, #f5f0ff 50%, #fce4ec 100%);
}
/* ========== 登录卡片容器:毛玻璃效果 ========== */
.login-card {
background: rgba(255, 255, 255, 0.85); /* 半透明白色背景 */
backdrop-filter: blur(20px); /* 毛玻璃模糊效果 */
-webkit-backdrop-filter: blur(20px); /* Safari 兼容 */
border: 1px solid rgba(255, 255, 255, 0.6);
border-radius: 20px;
padding: 48px 40px;
width: 100%;
max-width: 400px; /* 最大宽度限制 */
box-shadow: 0 8px 32px rgba(100, 120, 180, 0.12), 0 2px 8px rgba(0, 0, 0, 0.04);
}
/* ========== 登录页头部标题区域 ========== */
.login-header {
text-align: center;
margin-bottom: 36px;
}
.login-header h1 {
font-size: 26px;
font-weight: 600;
color: #1a1a2e;
letter-spacing: 0.5px;
}
.login-header p {
font-size: 14px;
color: #7c8db5; /* 浅灰色副标题 */
margin-top: 8px;
}
/* ========== 表单项通用样式 ========== */
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
font-size: 13px;
font-weight: 500;
color: #4a5568;
margin-bottom: 8px;
}
.form-group input {
width: 100%;
padding: 14px 16px;
font-size: 15px;
color: #1a1a2e;
background: #f7f9fc; /* 浅灰蓝输入框背景 */
border: 1.5px solid #e2e8f0;
border-radius: 12px;
outline: none;
transition: all 0.25s ease; /* 聚焦时的平滑过渡动画 */
}
/* 输入框占位符颜色 */
.form-group input::placeholder {
color: #a0aec0;
}
/* 输入框聚焦状态:蓝色边框 + 发光阴影 */
.form-group input:focus {
border-color: #6c8cff;
background: #fff;
box-shadow: 0 0 0 3px rgba(108, 140, 255, 0.12);
}
/* ========== 登录按钮 ========== */
.login-btn {
width: 100%;
padding: 14px;
font-size: 15px;
font-weight: 600;
color: #fff;
/* 蓝紫渐变按钮 */
background: linear-gradient(135deg, #6c8cff 0%, #a78bfa 100%);
border: none;
border-radius: 12px;
cursor: pointer;
transition: all 0.3s ease;
margin-top: 8px;
letter-spacing: 0.5px;
}
/* 按钮悬停:上浮 + 阴影加深 */
.login-btn:hover {
transform: translateY(-1px);
box-shadow: 0 6px 20px rgba(108, 140, 255, 0.35);
}
/* 按钮点击:恢复原位 */
.login-btn:active {
transform: translateY(0);
}
/* ========== 腾讯验证码容器(默认隐藏,按需显示) ========== */
.captcha-box {
margin-top: 24px;
}
/* ========== 手机短信验证码区域(默认隐藏) ========== */
.sms-captcha {
margin-top: 24px;
display: none; /* 初始隐藏,需要时由 JS 显示 */
}
.sms-captcha .form-group {
margin-bottom: 16px;
}
/* 短信验证按钮样式(与登录按钮风格一致) */
.sms-captcha .verify-btn {
width: 100%;
padding: 14px;
font-size: 15px;
font-weight: 600;
color: #fff;
background: linear-gradient(135deg, #6c8cff 0%, #a78bfa 100%);
border: none;
border-radius: 12px;
cursor: pointer;
transition: all 0.3s ease;
margin-top: 8px;
}
.sms-captcha .verify-btn:hover {
transform: translateY(-1px);
box-shadow: 0 6px 20px rgba(108, 140, 255, 0.35);
}
/* ========== 返回首页链接 ========== */
.back-link {
display: inline-flex;
align-items: center;
gap: 6px;
margin-top: 20px;
font-size: 14px;
color: #7c8db5;
text-decoration: none;
transition: color 0.25s ease;
}
.back-link:hover {
color: #6c8cff; /* 悬停时变为蓝色 */
}
</style>
</head>
<body>
<!-- ========== 登录卡片主容器 ========== -->
<div class="login-card">
<!-- 页面标题 -->
<div class="login-header">
<h1>欢迎登录</h1>
<p>请输入您的账号信息</p>
</div>
<!-- 用户名输入框 -->
<div class="form-group">
<label>用户名</label>
<label for="username"></label><input type="text" id="username" placeholder="请输入用户名" />
</div>
<!-- 密码输入框 -->
<div class="form-group">
<label>密码</label>
<label for="password"></label><input type="password" id="password" placeholder="请输入密码" />
</div>
<!-- 登录按钮 -->
<button class="login-btn" id="loginBtn">登 录</button>
<!-- 腾讯验证码挂载容器(由 JS 动态控制显示/隐藏) -->
<div class="captcha-box" id="captchaBox"></div>
<!-- 手机短信验证码输入区域(由 JS 动态控制显示/隐藏) -->
<div class="sms-captcha" id="smsCaptcha">
<div class="form-group">
<label>验证码</label>
<label for="smsCode"></label><input type="text" id="smsCode" placeholder="请输入验证码" maxlength="6" />
</div>
<button class="verify-btn" id="smsVerifyBtn">验证</button>
</div>
<!-- 返回首页链接 -->
<a class="back-link" href="./index.html">← 返回首页</a>
</div>
<script src="./fingerprint.js"></script>
<script>
/**
* ============================================================
* 登录页面主逻辑(纯 JS 模拟版,不依赖 WASM)
* ============================================================
*
* 与 login_captcha.html 的区别:
* - 本文件不加载 WASM,而是用纯 JavaScript 实现 sid/edt 的加密生成
* - 完全模拟 WASM 的行为数据采集和加密流程
* - 适用于无法使用 WASM 的环境(如部分移动端浏览器、自动化测试等)
*
* 整体流程:
* 1. 从 cookie 读取设备标识(mid、userid、dfid
* 2. 生成随机 WebGL 指纹和模拟鼠标行为数据
* 3. 将行为数据用 AES-128-CBC 加密得到 EDT
* 4. 将 AES 密钥用 RSA-OAEP 加密得到 SID
* 5. 调用登录接口,若需要二次验证则展示验证码
*/
// 从 fingerprint.js 获取生成函数
const { generateWebGLHash, generateEDTData, encryptSid, hexToBase64, ri } = fingerprint;
// ========== DOM 元素引用 ==========
const usernameInput = document.getElementById('username');
const passwordInput = document.getElementById('password');
const loginBtn = document.getElementById('loginBtn');
const captchaBox = document.getElementById('captchaBox'); // 腾讯验证码容器
const smsCaptcha = document.getElementById('smsCaptcha'); // 短信验证码区域
const smsCode = document.getElementById('smsCode'); // 短信验证码输入框
const smsVerifyBtn = document.getElementById('smsVerifyBtn'); // 短信验证按钮
/**
* 根据 id 获取 DOM 元素的简写函数
* @param {string} id - 元素 id
* @returns {HTMLElement}
*/
function $(id) {
return document.getElementById(id);
}
/**
* 动态加载外部 JS 脚本
* @param {string} url - 脚本地址
* @param {function} cb - 加载完成/失败的回调函数
*/
function loadScript(url, cb) {
const s = document.createElement('script');
s.src = url;
s.onload = function () {
cb();
};
s.onerror = function () {
cb(new Error('load failed'));
};
document.head.appendChild(s);
}
/**
* 从 cookie 中读取指定名称的值
* @param {string} name - cookie 键名
* @returns {string} cookie 值,不存在时返回 '0'
*/
function getCookie(name) {
const match = document.cookie.match(new RegExp('(?:^|; )' + name + '=([^;]*)'));
return match ? decodeURIComponent(match[1]) : '0';
}
// ========== 登录按钮点击事件 ==========
loginBtn.addEventListener('click', async () => {
const username = usernameInput.value;
const password = passwordInput.value;
// 从 cookie 中读取设备标识,不存在则默认 '0'
// cookie 键名与 server.js 中注入的一致
const mid = getCookie('KUGOU_API_MID'); // 设备 MIDserver.js 通过 calculateMid 生成)
const userid = getCookie('userid'); // 用户 ID(登录成功后由服务端 Set-Cookie 写入)
const dfid = getCookie('dfid'); // 设备指纹 IDregister_dev 接口返回后写入 cookie
// 随机化鼠标轨迹参数,使每次请求的行为指纹更接近真实用户
const points = ri(30, 60); // 鼠标轨迹采样点数(30~60 个点)
const startX = ri(200, 600); // 鼠标起点 X(页面中部区域)
const startY = ri(200, 500); // 鼠标起点 Y
const endX = ri(500, 700); // 鼠标终点 X(登录按钮附近)
const endY = ri(80, 150); // 鼠标终点 Y
// 生成 WebGL 指纹哈希和当前时间戳
const webglHash = generateWebGLHash();
const ts = Date.now();
// 生成行为数据(鼠标轨迹 + 滚动 + 窗口事件)
const data = generateEDTData({
startX,
startY,
endX,
endY,
mousePoints: points,
});
// 拼接完整明文
// 格式: mid=xxx;userid=xxx;dfid=xxx;webgl=xxx;webdriver=0;ts=xxx;data=xxx
const sidPlaintext = `mid=${mid};userid=${userid};dfid=${dfid};webgl=${webglHash};webdriver=0;ts=${ts};data=${data}`;
// 执行加密:AES 加密明文得到 EDT,RSA 加密 AES 密钥得到 SID
const result = await encryptSid(sidPlaintext);
// SID = RSA-OAEP 加密后的 AES key(用服务器公钥保护 AES key)
// EDT = AES-CBC 加密后的指纹明文(用 AES key 加密行为数据)
const sid = hexToBase64(result.rsaCiphertextHex);
const edt = hexToBase64(result.aesCiphertextHex);
console.log({ sid, edt });
let sessionid = '';
try {
// 第一步:调用登录接口
const loginPromise = await fetch(`/login?username=${encodeURIComponent(username)}&password=${encodeURIComponent(password)}`).then(
(response) => response.json()
);
// status === 1 表示登录成功
if (loginPromise.status === 1) {
alert('登录成功!');
} else {
// error_code === 20028 表示需要进行二次安全验证
// ssaCode 是服务端返回的验证事件 ID
if (loginPromise.error_code === 20028 && loginPromise.ssaCode) {
// 第二步:获取验证详情(验证类型、腾讯验证码 appid 等)
const verifyInfo = await fetch(`/get/verify/info?eventid=${loginPromise.ssaCode}`).then((response) => response.json());
console.log('Verify Info:', verifyInfo);
sessionid = verifyInfo.data.sessionid; // 验证会话 ID
const txappid = verifyInfo.data.txappid; // 腾讯验证码应用 ID
const business = verifyInfo.data.business; // 业务标识
const v_type = verifyInfo.data.v_type; // 验证类型(23=腾讯验证码,32=手机验证码)
if (v_type === 23) {
// ===== 腾讯图形验证码流程(v_type=23 =====
// 显示验证码容器,隐藏短信验证码区域
captchaBox.style.display = 'block';
smsCaptcha.style.display = 'none';
// 动态加载腾讯验证码 JS SDK
loadScript('https://turing.captcha.qcloud.com/TCaptcha.js', function () {
if (typeof TencentCaptcha === 'undefined') {
return;
}
var appId = txappid;
// 创建腾讯验证码实例
// 回调函数 res 包含验证结果:
// ret === 0 表示验证通过
// res.ticket: 验证票据
// res.randstr: 随机字符串(用于防重放)
var captcha = new TencentCaptcha(
appId,
function (res) {
console.log('Captcha response:', res);
if (res.ret === 0) {
// 验证通过:拼装验证数据并发送到服务端
// 格式:KGCodeTX|{ticket, randstr, txappid} 的 JSON 字符串
// KGCodeTX 是腾讯验证码的标识前缀
const a = 'KGCodeTX|' + JSON.stringify({ ticket: res.ticket, randstr: res.randstr, txappid: appId });
// 第三步:将验证码结果提交到服务端进行校验
fetch(
`/verify/user/info?eventid=${loginPromise.ssaCode}&v_type=${v_type}&verifycode=${encodeURIComponent(
a
)}&sid=${encodeURIComponent(sid)}&edt=${encodeURIComponent(edt)}`,
{
method: 'GET',
}
)
.then((response) => response.json())
.then((data) => {
console.log('Verification Result:', data);
})
.catch((error) => {
console.error('Verification request failed:', error);
alert('验证码验证请求失败');
});
} else {
// 用户关闭了验证码弹窗或验证失败
alert(`用户取消 (ret=${res.ret})`);
}
},
{
type: '', // 验证码类型(空字符串表示使用后台配置的默认类型)
showHeader: false, // 不显示验证码弹窗标题栏
ready: function () {}, // 验证码就绪回调(当前无操作)
}
);
console.log('Captcha initialized:', captcha);
captcha.show(); // 弹出验证码窗口
});
} else if (v_type === 32) {
// ===== 手机短信验证码流程(v_type=32 =====
// 隐藏腾讯验证码容器,显示短信验证码输入区域
captchaBox.style.display = 'none';
smsCaptcha.style.display = 'block';
smsCode.value = '';
smsCode.focus(); // 自动聚焦到验证码输入框
// 绑定短信验证按钮的点击事件
smsVerifyBtn.onclick = async function () {
const code = smsCode.value.trim();
if (!code) {
alert('请输入验证码');
return;
}
try {
// 第三步:将手机验证码提交到服务端校验
const verifyResponse = await fetch(
`/verify/user/info?eventid=${loginPromise.ssaCode}&v_type=${v_type}&verifycode=${encodeURIComponent(
code
)}&sid=${encodeURIComponent(sid)}&edt=${encodeURIComponent(edt)}`,
{
method: 'GET',
}
);
const verifyData = await verifyResponse.json();
console.log('SMS Verification Result:', verifyData);
alert('验证成功');
} catch (error) {
console.error('SMS Verification request failed:', error);
alert('验证码验证请求失败');
}
};
} else {
// 未知的验证类型
console.warn('未知的验证类型:', v_type);
alert('未知的验证类型');
}
}
}
} catch (e) {
console.warn('Login request failed:', e);
}
});
</script>
</body>
</html>
@@ -0,0 +1,394 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>SID / EDT 生成器</title>
<!--
============================================================
加密 API 拦截层(必须在 verifycode.js 之前加载)
============================================================
通过 monkey-patch SubtleCrypto.prototype 上的方法,拦截 WASM 内部的
加密/解密/签名/密钥导入等操作,记录到 window.__cryptoLog 数组,
用于调试和分析 WASM 的加密流程。
-->
<script>
/** 加密操作日志(拦截到的数据都存到这里) */
window.__cryptoLog = [];
/** CryptoKey → 导入时的元数据映射(WeakMap 避免内存泄漏) */
const __keyMap = new WeakMap();
/**
* ArrayBuffer / TypedArray 转 hex 字符串
* @param {ArrayBuffer|TypedArray} buf - 二进制缓冲区
* @returns {string} hex 字符串(每个字节两位,小写)
*/
function ab2hex(buf) {
if (!buf) return '';
let arr;
if (buf instanceof ArrayBuffer) arr = new Uint8Array(buf);
else if (ArrayBuffer.isView(buf)) arr = new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
else return String(buf);
let s = '';
for (let i = 0; i < arr.length; i++) s += arr[i].toString(16).padStart(2, '0');
return s;
}
/**
* ArrayBuffer 转 UTF-8 字符串(失败时回退为 hex)
* @param {ArrayBuffer} buf - 二进制缓冲区
* @returns {string} 解码后的字符串
*/
function ab2str(buf) {
try {
return new TextDecoder().decode(buf);
} catch (e) {
return ab2hex(buf);
}
}
/**
* 将算法描述对象序列化为 JSON 字符串
* 自动处理 ArrayBuffer/Uint8Array 类型的值,转为 hex
* @param {Object} a - Web Crypto API 的算法描述对象
* @returns {string} JSON 字符串
*/
function algoStr(a) {
try {
return JSON.stringify(a, function (k, v) {
if (v instanceof ArrayBuffer) return ab2hex(v);
if (v instanceof Uint8Array) return ab2hex(v);
return v;
});
} catch (e) {
return String(a);
}
}
</script>
<!-- 引入 WASM 验证码工具包(提供 wasm_bindgen 全局对象) -->
<script src="verify-pkg/verifycode.js"></script>
<script>
/**
* ============================================================
* Web Crypto API 拦截补丁
* ============================================================
*
* 在 verifycode.js 加载之后(wasm_bindgen.init 已定义)、
* wasm_bindgen() 调用之前(WASM 还没执行)注入,
* 这样 WASM 内部调用的加密操作都会被记录。
*
* 拦截的方法:
* - encrypt: AES/RSA 加密
* - decrypt: AES/RSA 解密
* - sign: RSA 签名
* - importKey: 密钥导入(记录原始密钥数据)
* - generateKey: 密钥生成
* - getRandomValues: 随机数生成
*/
// 保存原始方法引用
const origEncrypt = SubtleCrypto.prototype.encrypt;
const origDecrypt = SubtleCrypto.prototype.decrypt;
const origSign = SubtleCrypto.prototype.sign;
const origImportKey = SubtleCrypto.prototype.importKey;
const origGenerateKey = SubtleCrypto.prototype.generateKey;
/**
* 拦截 encrypt:记录算法、明文、密钥信息和密文
*/
SubtleCrypto.prototype.encrypt = function (algorithm, key, data) {
const ki = __keyMap.get(key);
const entry = {
op: 'encrypt',
algo: algoStr(algorithm),
plaintext: ab2str(data),
plaintextHex: ab2hex(data),
keyType: key.type,
keyAlgorithm: JSON.stringify(key.algorithm),
keyUsages: key.usages,
};
if (ki) {
entry.importedKey = ki.keyDataHex;
entry.importedAlgo = ki.algorithm;
}
window.__cryptoLog.push(entry);
const p = origEncrypt.call(this, algorithm, key, data);
p.then(function (result) {
entry.ciphertextHex = ab2hex(result);
entry.ciphertextLen = result.byteLength;
}).catch(function () {});
return p;
};
/**
* 拦截 decrypt:记录算法、密文和解密后的明文
*/
SubtleCrypto.prototype.decrypt = function (algorithm, key, data) {
const ki = __keyMap.get(key);
const entry = { op: 'decrypt', algo: algoStr(algorithm), ciphertextHex: ab2hex(data) };
if (ki) {
entry.importedKey = ki.keyDataHex;
entry.importedAlgo = ki.algorithm;
}
window.__cryptoLog.push(entry);
const p = origDecrypt.call(this, algorithm, key, data);
p.then(function (result) {
entry.plaintext = ab2str(result);
entry.plaintextHex = ab2hex(result);
}).catch(function () {});
return p;
};
/**
* 拦截 sign:记录算法、待签名数据和签名结果
*/
SubtleCrypto.prototype.sign = function (algorithm, key, data) {
const entry = { op: 'sign', algo: algoStr(algorithm), dataHex: ab2hex(data) };
window.__cryptoLog.push(entry);
const p = origSign.call(this, algorithm, key, data);
p.then(function (result) {
entry.signatureHex = ab2hex(result);
entry.signatureLen = result.byteLength;
}).catch(function () {});
return p;
};
/**
* 拦截 importKey:记录导入的密钥数据,并建立 CryptoKey → 元数据的映射
*/
SubtleCrypto.prototype.importKey = function (format, keyData, algorithm, extractable, usages) {
const ki = {
format: format,
keyDataHex: ab2hex(keyData),
algorithm: algoStr(algorithm),
usages: usages,
};
const p = origImportKey.call(this, format, keyData, algorithm, extractable, usages);
p.then(function (cryptoKey) {
__keyMap.set(cryptoKey, ki);
window.__cryptoLog.push({
op: 'importKey',
keyDataHex: ki.keyDataHex,
algorithm: ki.algorithm,
usages: ki.usages,
});
}).catch(function () {});
return p;
};
/**
* 拦截 generateKey:记录算法和用途
*/
SubtleCrypto.prototype.generateKey = function (algorithm, extractable, usages) {
window.__cryptoLog.push({ op: 'generateKey', algo: algoStr(algorithm), usages: usages });
return origGenerateKey.call(this, algorithm, extractable, usages);
};
/**
* 拦截 getRandomValues:记录随机数请求的长度
*/
const origGRV = crypto.getRandomValues;
crypto.getRandomValues = function (arr) {
window.__cryptoLog.push({ op: 'getRandomValues', len: arr.length });
return origGRV.call(crypto, arr);
};
</script>
<style>
/* ========== 全局重置 ========== */
* { margin: 0; padding: 0; box-sizing: border-box; }
/* ========== 页面主体 ========== */
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #f5f5f5;
padding: 40px 20px;
}
/* ========== 容器 ========== */
.container { max-width: 800px; margin: 0 auto; }
/* ========== 标题 ========== */
h2 { font-size: 20px; color: #333; margin-bottom: 20px; text-align: center; }
/* ========== 按钮 ========== */
.btn {
display: block; width: 100%; padding: 12px;
background: #1677ff; color: #fff; border: none; border-radius: 8px;
font-size: 15px; cursor: pointer; margin-bottom: 8px;
}
.btn:active { opacity: 0.8; }
.btn-clear { background: #999; }
/* ========== 结果区域 ========== */
.result { margin-top: 16px; }
.field { margin-bottom: 12px; }
.field label { display: block; font-size: 13px; color: #666; margin-bottom: 4px; }
.field .val {
background: #fff; border: 1px solid #e0e0e0; border-radius: 6px;
padding: 10px 12px; font-size: 12px; color: #333;
word-break: break-all; font-family: monospace;
min-height: 40px; white-space: pre-wrap;
max-height: 300px; overflow-y: auto;
}
/* ========== 日志区域 ========== */
.log-section { margin-top: 24px; }
.log-section h3 { font-size: 15px; color: #333; margin-bottom: 8px; }
.log-entry {
background: #fff; border: 1px solid #e0e0e0; border-radius: 8px;
padding: 12px; margin-bottom: 8px; font-size: 12px; font-family: monospace;
}
.log-entry .op {
display: inline-block; padding: 2px 8px; border-radius: 4px;
font-weight: 600; font-size: 11px; margin-bottom: 6px;
}
/* 日志条目类型颜色 */
.op-encrypt { background: #fff2f0; color: #ff4d4f; }
.op-importKey { background: #e6f4ff; color: #1677ff; }
.op-decrypt { background: #f6ffed; color: #52c41a; }
.op-generateKey { background: #fffbe6; color: #faad14; }
.op-sign { background: #f9f0ff; color: #722ed1; }
.op-getRandomValues { background: #f5f5f5; color: #666; }
/* 日志行 */
.log-row { display: flex; gap: 8px; margin-bottom: 3px; }
.log-label { color: #999; min-width: 100px; flex-shrink: 0; }
.log-value { color: #333; word-break: break-all; }
</style>
</head>
<body>
<div class="container">
<h2>SID / EDT 生成器</h2>
<!-- 操作按钮 -->
<button class="btn" id="generate">生成</button>
<button class="btn btn-clear" id="clearLog">清除日志</button>
<!-- 生成结果 -->
<div class="result">
<div class="field">
<label>sidRSA-OAEP 加密的 AES 密钥,Base64</label>
<div class="val" id="outSid"></div>
</div>
<div class="field">
<label>edtAES-128-CBC 加密的行为数据,Base64</label>
<div class="val" id="outEdt"></div>
</div>
</div>
<!-- 加密 API 拦截日志 -->
<div class="log-section">
<h3>Crypto API 拦截日志</h3>
<div id="logContainer"></div>
</div>
</div>
<script>
/**
* ============================================================
* SID / EDT 生成器主逻辑
* ============================================================
*
* 功能:
* 1. 加载 WASM 模块(verifycode_bg.wasm
* 2. 点击"生成"按钮,调用 wasm_bindgen.EData 生成 sid 和 edt
* 3. 渲染拦截到的 Crypto API 调用日志(加密、密钥导入等)
*
* 用途:
* - 调试 WASM 的加密流程,分析 sid/edt 的生成过程
* - 查看 AES 密钥、RSA 公钥、加密明文等中间数据
*/
(async () => {
// ========== DOM 元素引用 ==========
const btn = document.getElementById('generate'); // 生成按钮
const outSid = document.getElementById('outSid'); // sid 输出区域
const outEdt = document.getElementById('outEdt'); // edt 输出区域
const logContainer = document.getElementById('logContainer'); // 日志容器
/**
* 渲染加密 API 拦截日志
* 从 window.__cryptoLog 数组读取,按倒序排列(最新的在最上面)
*/
function renderLog() {
const logs = window.__cryptoLog;
if (!logs.length) {
logContainer.innerHTML = '<div style="color:#999;font-size:13px">暂无加密调用记录。</div>';
return;
}
let html = '';
for (let i = logs.length - 1; i >= 0; i--) {
const e = logs[i];
const opClass = 'op-' + (e.op || 'unknown');
html += '<div class="log-entry">';
html += '<div class="op ' + opClass + '">' + e.op + '</div>';
for (const k of Object.keys(e)) {
// 跳过内部字段
if (k === 'op' || k === 'timestamp' || k === 'keyId' || k === '_keyInfo') continue;
html += '<div class="log-row"><span class="log-label">' + k + ':</span><span class="log-value">' + e[k] + '</span></div>';
}
html += '</div>';
}
logContainer.innerHTML = html;
}
// 清除日志按钮
document.getElementById('clearLog').addEventListener('click', () => {
window.__cryptoLog.length = 0;
renderLog();
});
/**
* 初始化应用:加载 WASM 模块
*/
async function initApp() {
try {
if (typeof wasm_bindgen !== 'undefined') {
await wasm_bindgen('./verify-pkg/verifycode_bg.wasm');
wasm_bindgen.run();
console.log('[WASM] Loaded successfully');
}
} catch (e) {
console.warn('[WASM] initialization failed:', e);
}
renderLog();
// 生成按钮点击事件
btn.addEventListener('click', () => {
btn.disabled = true;
btn.textContent = '生成中...';
outSid.textContent = '';
outEdt.textContent = '';
setTimeout(() => {
try {
if (typeof wasm_bindgen !== 'undefined' && wasm_bindgen.EData) {
const e = new wasm_bindgen.EData();
outSid.textContent = e.get_sid();
outEdt.textContent = e.get_edt();
} else {
outSid.textContent = '(WASM 未加载)';
}
} catch (err) {
outSid.textContent = 'Error: ' + err.message;
}
// 等待异步 .then() 回调填充日志后再渲染
setTimeout(function () {
renderLog();
btn.disabled = false;
btn.textContent = '生成';
}, 200);
}, 50);
});
}
await initApp();
})();
</script>
</body>
</html>
File diff suppressed because it is too large Load Diff
Binary file not shown.
+679
View File
@@ -0,0 +1,679 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no" />
<title>验证码验证</title>
<style>
/* ========== 全局重置 ========== */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* ========== 页面主体:全屏居中 + 渐变背景 ========== */
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #e0ecff 0%, #f5f0ff 50%, #fce4ec 100%);
}
/* ========== 卡片容器:毛玻璃效果 ========== */
.login-card {
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.6);
border-radius: 20px;
padding: 48px 40px;
width: 100%;
max-width: 420px;
box-shadow: 0 8px 32px rgba(100, 120, 180, 0.12), 0 2px 8px rgba(0, 0, 0, 0.04);
transition: max-width 0.3s ease;
}
/* 第二步展开时卡片加宽 */
.login-card.wide {
max-width: 600px;
}
/* ========== 标题区域 ========== */
.login-header {
text-align: center;
margin-bottom: 36px;
}
.login-header h1 {
font-size: 26px;
font-weight: 600;
color: #1a1a2e;
letter-spacing: 0.5px;
}
.login-header p {
font-size: 14px;
color: #7c8db5;
margin-top: 8px;
}
/* ========== 表单项 ========== */
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
font-size: 13px;
font-weight: 500;
color: #4a5568;
margin-bottom: 8px;
}
.form-group input {
width: 100%;
padding: 14px 16px;
font-size: 15px;
color: #1a1a2e;
background: #f7f9fc;
border: 1.5px solid #e2e8f0;
border-radius: 12px;
outline: none;
transition: all 0.25s ease;
}
.form-group input::placeholder {
color: #a0aec0;
}
.form-group input:focus {
border-color: #6c8cff;
background: #fff;
box-shadow: 0 0 0 3px rgba(108, 140, 255, 0.12);
}
/* ========== 行内表单(两列布局) ========== */
.form-row {
display: flex;
gap: 12px;
}
.form-row .form-group {
flex: 1;
}
/* ========== 主按钮 ========== */
.login-btn {
width: 100%;
padding: 14px;
font-size: 15px;
font-weight: 600;
color: #fff;
background: linear-gradient(135deg, #6c8cff 0%, #a78bfa 100%);
border: none;
border-radius: 12px;
cursor: pointer;
transition: all 0.3s ease;
margin-top: 8px;
}
.login-btn:hover {
transform: translateY(-1px);
box-shadow: 0 6px 20px rgba(108, 140, 255, 0.35);
}
.login-btn:active {
transform: translateY(0);
}
.login-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
/* ========== 验证码容器 ========== */
.captcha-box {
margin-top: 24px;
}
/* ========== 短信验证码区域(默认隐藏) ========== */
.sms-captcha {
margin-top: 24px;
display: none;
}
.sms-captcha .form-group {
margin-bottom: 16px;
}
.sms-captcha .verify-btn {
width: 100%;
padding: 14px;
font-size: 15px;
font-weight: 600;
color: #fff;
background: linear-gradient(135deg, #6c8cff 0%, #a78bfa 100%);
border: none;
border-radius: 12px;
cursor: pointer;
transition: all 0.3s ease;
margin-top: 8px;
}
.sms-captcha .verify-btn:hover {
transform: translateY(-1px);
box-shadow: 0 6px 20px rgba(108, 140, 255, 0.35);
}
/* ========== 结果提示 ========== */
.result {
margin-top: 12px;
padding: 12px;
border-radius: 8px;
text-align: center;
font-size: 13px;
display: none;
}
.result.ok {
background: #f6ffed;
color: #52c41a;
border: 1px solid #b7eb8f;
}
.result.err {
background: #fff2f0;
color: #ff4d4f;
border: 1px solid #ffccc7;
}
/* ========== 验证信息面板 ========== */
.verify-info {
margin-top: 12px;
padding: 12px;
background: #f5f5f5;
border-radius: 8px;
font-size: 12px;
color: #666;
display: none;
}
.verify-info .vi-row {
display: flex;
justify-content: space-between;
align-items: baseline;
padding: 2px 0;
}
.verify-info .vi-label {
color: #999;
flex-shrink: 0;
}
.verify-info .vi-value {
color: #333;
font-family: monospace;
text-align: right;
max-width: 72%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* ========== 返回链接 ========== */
.back-link {
display: inline-flex;
align-items: center;
gap: 6px;
margin-top: 20px;
font-size: 14px;
color: #7c8db5;
text-decoration: none;
transition: color 0.25s ease;
}
.back-link:hover {
color: #6c8cff;
}
</style>
</head>
<body>
<div class="login-card">
<!-- 页面标题 -->
<div class="login-header">
<h1>验证码验证</h1>
<p>腾讯验证码 / 手机短信验证</p>
</div>
<!-- ========== 第一步:输入 eventid / ssaCode ========== -->
<div id="step1">
<div class="form-group">
<label>eventid / ssaCode</label>
<label for="eventidInput"></label><input type="text" id="eventidInput" placeholder="请输入 eventid / ssaCode" autocomplete="off" />
</div>
<button class="login-btn" id="getInfoBtn">获取验证信息</button>
<div class="result" id="step1Result" style="display: none; margin-top: 12px"></div>
</div>
<!-- ========== 第二步:显示验证信息 + 设备参数,手动触发验证码 ========== -->
<div id="step2" style="display: none">
<!-- 验证信息面板(sessionid、v_type、txappid 等) -->
<div class="verify-info" id="verifyInfo"></div>
<!-- 设备参数:mid、userid、dfid、webgl(从 cookie 预填,可手动修改) -->
<div class="form-row">
<div class="form-group">
<label>mid</label>
<label for="midInput"></label><input type="text" id="midInput" />
</div>
<div class="form-group">
<label>userid</label>
<label for="useridInput"></label><input type="text" id="useridInput" />
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>dfid</label>
<label for="dfidInput"></label><input type="text" id="dfidInput" />
</div>
<div class="form-group">
<label>webgl</label>
<label for="webglInput"></label><input type="text" id="webglInput" />
</div>
</div>
<!-- 触发验证码按钮 -->
<button class="login-btn" id="triggerCaptchaBtn"></button>
<!-- 腾讯验证码挂载容器 -->
<div class="captcha-box" id="captchaBox"></div>
<!-- 短信验证码输入区域 -->
<div class="sms-captcha" id="smsCaptcha">
<div class="form-group">
<label>验证码</label>
<label for="smsCode"></label><input type="text" id="smsCode" placeholder="请输入验证码" maxlength="6" />
</div>
<button class="verify-btn" id="smsVerifyBtn">验证</button>
</div>
<!-- 验证结果提示 -->
<div class="result" id="result"></div>
</div>
<a class="back-link" href="./index.html">← 返回首页</a>
</div>
<!-- 引入行为指纹生成工具 -->
<script src="./fingerprint.js"></script>
<script>
/**
* ============================================================
* 验证码验证页面
* ============================================================
*
* 两步流程:
* 1. 输入 eventid/ssaCode → 调用 /get/verify/info 获取验证信息
* 2. 显示设备参数(mid/userid/dfid/webgl,从 cookie 预填)→ 手动触发验证码
*
* 支持的验证类型:
* - v_type=23:腾讯图形验证码
* - v_type=32:手机短信验证码
*/
// ========== 从 fingerprint.js 解构工具函数 ==========
const { generateWebGLHash, generateEDTData, encryptSid, hexToBase64, ri } = fingerprint;
// ========== DOM 元素引用 ==========
const eventidInput = document.getElementById('eventidInput'); // eventid 输入框
const getInfoBtn = document.getElementById('getInfoBtn'); // 获取验证信息按钮
const step1Result = document.getElementById('step1Result'); // 第一步结果提示
const step1 = document.getElementById('step1'); // 第一步容器
const step2 = document.getElementById('step2'); // 第二步容器
const verifyInfoEl = document.getElementById('verifyInfo'); // 验证信息面板
const midInput = document.getElementById('midInput'); // 设备 MID 输入框
const useridInput = document.getElementById('useridInput'); // 用户 ID 输入框
const dfidInput = document.getElementById('dfidInput'); // 设备指纹 ID 输入框
const webglInput = document.getElementById('webglInput'); // WebGL 指纹输入框
const triggerCaptchaBtn = document.getElementById('triggerCaptchaBtn'); // 触发验证码按钮
const captchaBox = document.getElementById('captchaBox'); // 腾讯验证码容器
const smsCaptcha = document.getElementById('smsCaptcha'); // 短信验证码区域
const smsCode = document.getElementById('smsCode'); // 短信验证码输入框
const smsVerifyBtn = document.getElementById('smsVerifyBtn'); // 短信验证按钮
const resultEl = document.getElementById('result'); // 验证结果提示
// ========== 验证信息缓存 ==========
let currentEventid = ''; // 当前 eventid
let currentTxappid = ''; // 腾讯验证码应用 ID
let currentVtype = 0; // 验证类型(23=腾讯, 32=短信)
/**
* 从 cookie 中读取指定名称的值
* @param {string} name - cookie 键名
* @returns {string} cookie 值,不存在时返回 '0'
*/
function getCookie(name) {
const match = document.cookie.match(new RegExp('(?:^|; )' + name + '=([^;]*)'));
return match ? decodeURIComponent(match[1]) : '0';
}
/**
* 显示第一步的结果提示
* @param {boolean} ok - 是否成功
* @param {string} text - 提示文本
*/
function showStep1Result(ok, text) {
step1Result.style.display = 'block';
step1Result.className = 'result ' + (ok ? 'ok' : 'err');
step1Result.textContent = text;
}
/**
* 显示第二步的验证结果提示
* @param {boolean} ok - 是否成功
* @param {string} text - 提示文本
*/
function showResult(ok, text) {
resultEl.style.display = 'block';
resultEl.className = 'result ' + (ok ? 'ok' : 'err');
resultEl.textContent = text;
}
/**
* 动态加载外部 JS 脚本
* @param {string} url - 脚本地址
* @param {function} cb - 加载完成/失败的回调函数
*/
function loadScript(url, cb) {
const s = document.createElement('script');
s.src = url;
s.onload = function () {
cb();
};
s.onerror = function () {
cb(new Error('load failed'));
};
document.head.appendChild(s);
}
/**
* ============================================================
* 第一步:获取验证信息
* ============================================================
*
* 调用 /get/verify/info 接口获取验证详情(sessionid、v_type、txappid 等),
* 成功后切换到第二步,显示设备参数输入框。
*/
async function fetchVerifyInfo() {
const eventid = eventidInput.value.trim();
if (!eventid) {
eventidInput.focus();
return;
}
// 按钮状态:禁用 + 文案变为"获取中..."
getInfoBtn.disabled = true;
getInfoBtn.textContent = '获取中...';
step1Result.style.display = 'none';
try {
// 调用服务端获取验证信息
const data = await fetch(`/get/verify/info?eventid=${encodeURIComponent(eventid)}`).then((r) => r.json());
console.log('Verify Info:', data);
// 接口返回失败
if (!data.data) {
showStep1Result(false, '获取失败: ' + (data.error_msg || JSON.stringify(data)));
getInfoBtn.disabled = false;
getInfoBtn.textContent = '获取验证信息';
return;
}
const info = data.data;
// 缓存验证信息
currentEventid = eventid;
currentTxappid = info.txappid || '';
currentVtype = info.v_type || 0;
// 渲染验证信息面板
verifyInfoEl.innerHTML = [
{ label: 'sessionid', value: info.sessionid },
{ label: 'v_type', value: info.v_type },
{ label: 'txappid', value: info.txappid },
{ label: 'business', value: info.business },
{ label: 'url', value: info.url },
]
.map(
(item) =>
`<div class="vi-row">` +
`<span class="vi-label">${item.label}</span>` +
`<span class="vi-value" title="${item.value || ''}">${item.value || '-'}</span>` +
`</div>`
)
.join('');
verifyInfoEl.style.display = 'block';
// 从 cookie 预填设备参数,读不到则默认 '0'
midInput.value = getCookie('KUGOU_API_MID') || '0';
useridInput.value = getCookie('userid') || '0';
dfidInput.value = getCookie('dfid') || '0';
// WebGL 指纹:优先读 cookie,读不到则调用 generateWebGLHash 生成
const webglCookie = getCookie('KUGOU_API_WEBGL');
webglInput.value = webglCookie && webglCookie !== '0' ? webglCookie : generateWebGLHash();
// 根据验证类型设置按钮文案
if (currentVtype === 23) {
triggerCaptchaBtn.textContent = '打开腾讯验证码';
} else if (currentVtype === 32) {
triggerCaptchaBtn.textContent = '发送短信验证码';
} else {
triggerCaptchaBtn.textContent = '未知验证类型: ' + currentVtype;
triggerCaptchaBtn.disabled = true;
}
// 切换到第二步,卡片加宽
step1.style.display = 'none';
step2.style.display = 'block';
document.querySelector('.login-card').classList.add('wide');
} catch (e) {
console.error('fetchVerifyInfo error:', e);
showStep1Result(false, '请求失败: ' + e.message);
getInfoBtn.disabled = false;
getInfoBtn.textContent = '获取验证信息';
}
}
/**
* ============================================================
* 生成 SID/EDT 加密数据
* ============================================================
*
* 从输入框读取设备参数,生成模拟行为指纹数据并加密:
* - SIDRSA-OAEP 加密的 AES 密钥(Base64
* - EDTAES-128-CBC 加密的行为数据(Base64
*
* @returns {Promise<{sid: string, edt: string}>}
*/
async function generateSidEdt() {
const mid = midInput.value.trim() || '0';
const userid = useridInput.value.trim() || '0';
const dfid = dfidInput.value.trim() || '0';
const webglHash = webglInput.value.trim() || generateWebGLHash();
// 随机化鼠标轨迹参数
const points = ri(30, 60); // 采样点数
const startX = ri(200, 600); // 起点 X
const startY = ri(200, 500); // 起点 Y
const endX = ri(500, 700); // 终点 X
const endY = ri(80, 150); // 终点 Y
const ts = Date.now();
// 生成模拟行为数据(鼠标轨迹 + 滚动 + 窗口事件)
const data = generateEDTData({ startX, startY, endX, endY, mousePoints: points });
// 拼接明文: mid=xxx;userid=xxx;dfid=xxx;webgl=xxx;webdriver=0;ts=xxx;data=xxx
const sidPlaintext = `mid=${mid};userid=${userid};dfid=${dfid};webgl=${webglHash};webdriver=0;ts=${ts};data=${data}`;
// AES 加密明文得到 EDTRSA 加密 AES 密钥得到 SID
const result = await encryptSid(sidPlaintext);
return {
sid: hexToBase64(result.rsaCiphertextHex),
edt: hexToBase64(result.aesCiphertextHex),
};
}
/**
* ============================================================
* 触发验证码(根据 v_type 分发)
* ============================================================
*/
async function triggerCaptcha() {
triggerCaptchaBtn.disabled = true;
resultEl.style.display = 'none';
// 生成 sid/edt
const { sid, edt } = await generateSidEdt();
console.log({ sid, edt });
// 根据验证类型分发
if (currentVtype === 23) {
startTxCaptcha(currentEventid, currentTxappid, sid, edt);
} else if (currentVtype === 32) {
startSmsCaptcha(currentEventid, currentVtype, sid, edt);
}
}
/**
* ============================================================
* 腾讯图形验证码流程(v_type=23)
* ============================================================
*
* 1. 动态加载 TCaptcha.js SDK
* 2. 创建 TencentCaptcha 实例并弹出
* 3. 用户验证通过后,将 ticket/randstr 拼成 verifycode 提交到服务端
*
* @param {string} eventid - 验证事件 ID
* @param {string} txappid - 腾讯验证码应用 ID
* @param {string} sid - RSA 加密的 AES 密钥
* @param {string} edt - AES 加密的行为数据
*/
function startTxCaptcha(eventid, txappid, sid, edt) {
captchaBox.style.display = 'block';
smsCaptcha.style.display = 'none';
loadScript('https://turing.captcha.qcloud.com/TCaptcha.js', function () {
if (typeof TencentCaptcha === 'undefined') {
showResult(false, 'TCaptcha.js 加载失败');
triggerCaptchaBtn.disabled = false;
return;
}
// 创建腾讯验证码实例
const captcha = new TencentCaptcha(
txappid,
function (res) {
console.log('Captcha response:', res);
if (res.ret === 0) {
// 验证通过:拼装 verifycode(格式:KGCodeTX|{ticket,randstr,txappid}
const verifycode =
'KGCodeTX|' +
JSON.stringify({
ticket: res.ticket,
randstr: res.randstr,
txappid: txappid,
});
// 提交到服务端校验
fetch(
`/verify/user/info?eventid=${eventid}&v_type=23` +
`&verifycode=${encodeURIComponent(verifycode)}` +
`&sid=${encodeURIComponent(sid)}&edt=${encodeURIComponent(edt)}`,
{ method: 'GET' }
)
.then((r) => r.json())
.then((data) => {
console.log('Verification Result:', data);
showResult(true, '验证成功!');
triggerCaptchaBtn.style.display = 'none';
})
.catch((err) => {
console.error('Verification request failed:', err);
showResult(false, '验证码验证请求失败');
triggerCaptchaBtn.disabled = false;
});
} else {
// 用户关闭验证码或验证失败
showResult(false, '用户取消 (ret=' + res.ret + ')');
triggerCaptchaBtn.disabled = false;
}
},
{ type: '', showHeader: false, ready: function () {} }
);
captcha.show();
});
}
/**
* ============================================================
* 手机短信验证码流程(v_type=32)
* ============================================================
*
* 1. 显示短信验证码输入框
* 2. 用户输入验证码后提交到服务端校验
*
* @param {string} eventid - 验证事件 ID
* @param {number} v_type - 验证类型
* @param {string} sid - RSA 加密的 AES 密钥
* @param {string} edt - AES 加密的行为数据
*/
function startSmsCaptcha(eventid, v_type, sid, edt) {
captchaBox.style.display = 'none';
smsCaptcha.style.display = 'block';
smsCode.value = '';
smsCode.focus();
// 绑定验证按钮点击事件
smsVerifyBtn.onclick = async function () {
const code = smsCode.value.trim();
if (!code) {
alert('请输入验证码');
return;
}
try {
// 提交短信验证码到服务端
const verifyResponse = await fetch(
`/verify/user/info?eventid=${eventid}&v_type=${v_type}` +
`&verifycode=${encodeURIComponent(code)}` +
`&sid=${encodeURIComponent(sid)}&edt=${encodeURIComponent(edt)}`,
{ method: 'GET' }
);
const verifyData = await verifyResponse.json();
console.log('SMS Verification Result:', verifyData);
showResult(true, '验证成功!');
} catch (err) {
console.error('SMS Verification request failed:', err);
showResult(false, '验证码验证请求失败');
}
};
}
// ========== 事件绑定 ==========
// 获取验证信息按钮
getInfoBtn.addEventListener('click', fetchVerifyInfo);
// eventid 输入框回车触发
eventidInput.addEventListener('keydown', function (e) {
if (e.key === 'Enter') fetchVerifyInfo();
});
// 触发验证码按钮
triggerCaptchaBtn.addEventListener('click', triggerCaptcha);
// ========== 页面初始化:从 URL 参数读取 eventid ==========
(function () {
const params = new URLSearchParams(window.location.search);
const eventid = params.get('eventid') || params.get('ssaCode') || '';
if (eventid) {
// URL 带有 eventid,自动填入并获取验证信息
eventidInput.value = eventid;
fetchVerifyInfo();
} else {
// 无 eventid,聚焦输入框等待用户输入
eventidInput.focus();
}
})();
</script>
</body>
</html>