Files
jiapu/public/js/captcha-pages.js
T

310 lines
9.5 KiB
JavaScript

(function (root, factory) {
// 验证码适配层同时支持浏览器运行和 Node 单元测试。
if (typeof module === 'object' && module.exports) {
module.exports = factory(root);
return;
}
root.CaptchaPages = factory(root);
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
'use strict';
var DEFAULT_SCENE_CODE = 'WEB_H5_LOGIN';
var CAPTCHA_LAYER_AREA = ['318px', '318px'];
var captchaLayerSeed = 0;
function getApi() {
return root.GenealogyApi && root.GenealogyApi.defaultClient;
}
function query(selector, rootNode) {
return (rootNode || root.document).querySelector(selector);
}
function showMessage(message) {
if (root.MessageUtil && root.MessageUtil.show) {
root.MessageUtil.show(message);
return;
}
if (root.console && root.console.warn) root.console.warn(message);
}
function buildChallengeBody(options, api) {
// 请求体字段严格对应 PC YAML 的 VerificationChallengeBody。
var settings = options || {};
return {
tenantId: settings.tenantId || api.tenantId,
clientId: settings.clientId || api.clientId,
sceneCode: settings.sceneCode || DEFAULT_SCENE_CODE,
subject: settings.subject || ''
};
}
function normalizeChallengeData(data) {
// TAC 需要 data.type、data.id 和图片字段;接口文档返回 captchaType、challengeId、payload。
var source = data || {};
var payload = source.payload || {};
return Object.assign({}, payload, {
type: source.captchaType || payload.type || 'SLIDER',
id: payload.id || source.challengeId,
challengeId: source.challengeId,
providerCode: source.providerCode || 'tianai',
captchaType: source.captchaType || payload.type || 'SLIDER'
});
}
function adaptChallengeResponse(response) {
var payload = response || {};
if (Number(payload.code) !== 200 || !payload.data) return payload;
return Object.assign({}, payload, {
data: normalizeChallengeData(payload.data)
});
}
function buildVerifyBody(requestData, context, api) {
// requestData 是 TAC 拖动完成后的轨迹数据;context 保存本次挑战的接口字段。
var settings = context || {};
var source = requestData || {};
return {
tenantId: settings.tenantId || api.tenantId,
clientId: settings.clientId || api.clientId,
sceneCode: settings.sceneCode || DEFAULT_SCENE_CODE,
subject: settings.subject || '',
challengeId: settings.challengeId || source.challengeId || source.id,
providerCode: settings.providerCode || 'tianai',
captchaType: settings.captchaType || 'SLIDER',
payload: {
id: source.id,
data: source.data || {}
}
};
}
function normalizeVerifyResponse(response) {
// TAC 只看 code 是否为 200;接口通过 passed 表示业务验证结果。
var payload = response || {};
if (Number(payload.code) === 200 && payload.data && payload.data.passed === false) {
return Object.assign({}, payload, {
code: 4001,
msg: payload.data.message || payload.msg || '验证失败'
});
}
return payload;
}
function extractValidToken(response) {
var payload = response || {};
var data = payload.data || {};
if (Number(payload.code) !== 200) return '';
if (data.passed === false) return '';
return data.validToken || payload.validToken || '';
}
function getTokenField(form) {
return query('input[name="validToken"]', form);
}
function createCaptchaLayer() {
// Layui 只负责提供默认弹层容器,验证码外观完全由 TAC 自带文件决定。
if (!root.layui || !root.layui.layer || !root.layui.layer.open || !root.document) {
throw new Error('缺少 layui.layer 验证码弹层依赖');
}
var layerId = 'captcha-layer-' + (++captchaLayerSeed);
var selector = '[data-captcha-layer-box="' + layerId + '"]';
var layerIndex = root.layui.layer.open({
type: 1,
title: false,
closeBtn: 0,
// 尺寸来自 TAC 原生 #tianai-captcha-parent,提前交给 Layui 计算居中位置。
area: CAPTCHA_LAYER_AREA,
offset: 'auto',
content: '<div data-captcha-layer-box="' + layerId + '"></div>'
});
var box = query(selector);
if (!box) {
root.layui.layer.close(layerIndex);
throw new Error('缺少 Layui 验证码实际挂载点');
}
return {
box: box,
layerIndex: layerIndex
};
}
function closeCaptchaLayer(captchaLayer) {
// 组件结束后清空第三方 DOM 并关闭本次创建的 Layui 层。
if (!captchaLayer) return;
if (captchaLayer.box) captchaLayer.box.innerHTML = '';
if (root.layui && root.layui.layer && root.layui.layer.close) {
root.layui.layer.close(captchaLayer.layerIndex);
}
}
function writeValidToken(form, token) {
var field = getTokenField(form);
if (field) field.value = token || '';
}
function shouldRequireCaptcha(result) {
// /captcha/require 返回 required=false 时允许直接继续业务请求。
if (!result) return true;
return result.required !== false;
}
function createTac(form, options, api, resolve, reject) {
var settings = options || {};
var captchaLayer;
var box;
var context = buildChallengeBody(settings, api);
var lastChallenge = {};
var captcha;
var config;
if (!root.TAC || !root.CaptchaConfig) {
reject(new Error('滑动验证组件未加载'));
return null;
}
try {
captchaLayer = createCaptchaLayer();
box = captchaLayer.box;
} catch (error) {
reject(error);
return null;
}
config = new root.CaptchaConfig({
bindEl: box,
requestCaptchaDataUrl: api.buildApiUrl('/captcha/challenge'),
validCaptchaUrl: api.buildApiUrl('/captcha/verify'),
requestHeaders: {
clientid: api.clientId
},
validSuccess: function (response, captchaControl, captchaInstance) {
var token = extractValidToken(response);
if (!token) {
if (captchaInstance && captchaInstance.destroyWindow) captchaInstance.destroyWindow();
closeCaptchaLayer(captchaLayer);
reject(new Error('滑动验证未返回有效票据'));
return;
}
writeValidToken(form, token);
if (captchaInstance && captchaInstance.destroyWindow) captchaInstance.destroyWindow();
closeCaptchaLayer(captchaLayer);
resolve(token);
},
validFail: function (response, captchaControl, captchaInstance) {
if (captchaInstance && captchaInstance.reloadCaptcha) captchaInstance.reloadCaptcha();
showMessage((response && (response.msg || response.message)) || '滑动验证失败,请重试');
},
btnCloseFun: function (event, captchaInstance) {
if (captchaInstance) captchaInstance.destroyWindow();
closeCaptchaLayer(captchaLayer);
reject(new Error('已取消滑动验证'));
}
});
config.addRequestChain({
preRequest: function (type, request) {
if (type === 'requestCaptchaData') {
request.data = buildChallengeBody(settings, api);
}
if (type === 'validCaptcha') {
request.data = buildVerifyBody(request.data, Object.assign({}, context, lastChallenge), api);
}
return true;
},
postRequest: function (type, request, response) {
if (type === 'requestCaptchaData') {
var adapted = adaptChallengeResponse(response);
lastChallenge = {
challengeId: adapted.data && adapted.data.challengeId,
providerCode: adapted.data && adapted.data.providerCode,
captchaType: adapted.data && adapted.data.captchaType
};
Object.keys(response || {}).forEach(function (key) {
delete response[key];
});
Object.assign(response, adapted);
}
if (type === 'validCaptcha') {
Object.assign(response, normalizeVerifyResponse(response));
}
return true;
}
});
try {
captcha = new root.TAC(config);
captcha.init();
} catch (error) {
closeCaptchaLayer(captchaLayer);
reject(error);
return null;
}
return captcha;
}
async function ensureToken(form, options) {
// 认证页提交前调用:已有 validToken 直接通过,否则按验证中心策略弹出 TAC。
var api = getApi();
var field = getTokenField(form);
var settings = options || {};
var requirement;
if (!api) return '';
if (field && field.value) return field.value;
if (!settings.sceneCode) return '';
try {
requirement = await api.captchaRequirement(buildChallengeBody(settings, api));
if (!shouldRequireCaptcha(requirement)) return 'CAPTCHA_NOT_REQUIRED';
} catch (error) {
// 查询策略失败时仍尝试直接拉起验证,避免认证接口缺少 validToken。
requirement = null;
}
return new Promise(function (resolve, reject) {
createTac(form, settings, api, resolve, reject);
}).catch(function (error) {
showMessage(error.message || '请先完成滑动验证');
return '';
});
}
return {
buildChallengeBody: buildChallengeBody,
adaptChallengeResponse: adaptChallengeResponse,
buildVerifyBody: buildVerifyBody,
createCaptchaLayer: createCaptchaLayer,
closeCaptchaLayer: closeCaptchaLayer,
extractValidToken: extractValidToken,
normalizeVerifyResponse: normalizeVerifyResponse,
shouldRequireCaptcha: shouldRequireCaptcha,
ensureToken: ensureToken
};
});