refactor: 重写checkToken V2模块

This commit is contained in:
MoeFurina
2026-08-07 11:52:54 +08:00
parent 7753b5de41
commit c4a3d83abe
7 changed files with 598 additions and 65 deletions
+1 -1
View File
@@ -239,7 +239,7 @@ module.exports = async (query, request) => {
const res = await request(
`/api/ad/listening/rights/gain`,
data,
createOption(query, 'xeapi', 'v2'),
createOption(query, 'eapi', 'v2'),
)
return {
+1 -1
View File
@@ -19,6 +19,6 @@ module.exports = (query, request) => {
return request(
`/api/middle/play/do/lottery`,
data,
createOption(query, 'eapi', 'v3'),
createOption(query, 'eapi', 'v2'),
)
}
+106 -12
View File
@@ -1,33 +1,126 @@
// 易盾反作弊 Token 注册端点
// 调用后获取实时 token 并存入共享存储,供后续带 checkToken 的请求使用
// 通过易盾官方 Watchman SDK(Web 版,跑在 jsdom 模拟的浏览器环境里)
// 实时调用 getToken(businessId) 获取反作弊 token,供后续带 checkToken 的请求使用
//
// GET /register/checktoken → 实时获取新 token(不缓存)
// POST /register/checktoken → 实时获取新 token
// GET /register/checktoken/v2 → 实时获取新 token(不缓存)
// POST /register/checktoken/v2 → 实时获取新 token
//
// 原理:加载 tool.min.js 初始化 Watchman,
// 再调用 wm.getToken(businessId) 拿 token。watchman 内部会 POST /v3/d + /v3/b
// 并携带加密的设备指纹与签名,无法用普通 HTTP 复刻,故走完整 SDK。
//
// 注意:每次获取都不缓存,模拟真实客户端每次请求使用新鲜 token,
// 避免反作弊 token 复用触发风控。
//
const { JSDOM, VirtualConsole } = require('jsdom')
const { default: axios } = require('axios')
const { APP_CONF } = require('../util/config.json')
const logger = require('../util/logger')
const URL = APP_CONF.dunDomainV2 + '/v2/config/js?pn=YD00000558929251'
// 网易云音乐在易盾的 productNumber 与 businessId
const PRODUCT_NUMBER = 'YD00000558929251'
const BUSINESS_ID = 'bd5d2f973ef74cd2a61325a412ae54d9'
const TOOL_JS_URL = `${APP_CONF.dunStaticDomain}/tool.min.js`
async function fetch() {
const res = await axios.get(URL, { timeout: 10000 })
const data = res.data
if (data && data.code === 200 && data.result && data.result.conf) {
return data.result.conf
// 最小 HTML 外壳,模拟网页环境
const HTML =
'<!doctype html><html><head><meta charset="UTF-8"></head><body></body></html>'
let toolJs = ''
let wm = null // Watchman 实例(进程内复用,可反复 getToken)
let initPromise = null
// 获取 tool.min.js(内存缓存,避免每次初始化重复下载)
async function getToolJs() {
if (toolJs) return toolJs
const res = await axios.get(TOOL_JS_URL, { timeout: 10000 })
toolJs = String(res.data)
return toolJs
}
// 初始化 Watchman(进程内只初始化一次,实例可反复 getToken)
async function ensureWatchman() {
if (wm) return wm
if (initPromise) return initPromise
initPromise = (async () => {
const js = await getToolJs()
const virtualConsole = new VirtualConsole()
virtualConsole.on('jsdomError', () => {})
const dom = new JSDOM(HTML, {
url: 'https://music.163.com/',
referrer: 'https://music.163.com/',
contentType: 'text/html',
runScripts: 'dangerously',
resources: 'usable', // 允许动态加载 watchman.min.js / JSONP
pretendToBeVisual: true,
virtualConsole,
beforeParse(window) {
// 抹掉 headless 特征,避免易盾风控误判
Object.defineProperty(window.navigator, 'webdriver', {
get: () => undefined,
})
window.chrome = { runtime: {} }
window.navigator.languages = ['zh-CN', 'zh']
window.navigator.plugins = [1, 2, 3, 4, 5]
},
})
const script = dom.window.document.createElement('script')
script.textContent = js
dom.window.document.body.appendChild(script)
return new Promise((resolve, reject) => {
const timer = setTimeout(
() => reject(new Error('watchman 初始化超时')),
15000,
)
dom.window.initWatchman({
auto: true,
productNumber: PRODUCT_NUMBER,
onload(instance) {
clearTimeout(timer)
wm = instance
resolve(instance)
},
onerror(...args) {
clearTimeout(timer)
reject(new Error('watchman 初始化失败'))
},
})
})
})()
try {
return await initPromise
} catch (e) {
// 初始化失败后允许下次重试
initPromise = null
wm = null
throw e
}
throw new Error('易盾返回异常: ' + JSON.stringify(data).substring(0, 200))
}
// 获取新 token
async function fetchToken() {
const instance = await ensureWatchman()
return new Promise((resolve) => {
const timer = setTimeout(() => resolve(''), 15000)
instance.getToken(BUSINESS_ID, (tk) => {
clearTimeout(timer)
resolve(typeof tk === 'string' ? tk : '')
})
})
}
// 端点处理:每次实时获取新 token
module.exports = async () => {
let token = ''
try {
token = await fetch()
token = await fetchToken()
} catch (e) {
// token 获取失败时返回空,由调用方决定是否重试
logger.warn('[checkToken v2]', e.message)
}
return {
status: 200,
@@ -38,8 +131,9 @@ module.exports = async () => {
// 给 request.js 读取用:每次调用实时获取新 token,不缓存
module.exports.getToken = async () => {
try {
return await fetch()
return await fetchToken()
} catch (e) {
logger.warn('[checkToken v2]', e.message)
return ''
}
}