Files
console-dev/app/lib/useCaptcha.js
T

217 lines
6.6 KiB
JavaScript

/**
* 登录/注册等场景的人机验证状态:先读站点开关,再识别图片或极验提供商。
* /captcha API 走统一后端 origin,但图片刷新 /captcha/tp_captcha/... 始终走前端同源反代。
*/
import { onMounted } from "vue";
import { watch } from "vue";
import { onScopeDispose } from "vue";
import { ref } from "vue";
import { ApiError } from "~/lib/auth.js";
import { useApi } from "~/lib/auth.js";
import { t as useSiteConfig_t } from "~/lib/useSiteConfig.js";
var s = (e) => e === !0 || e === 1 || e === `1`;
// 登录可由全局开关或后端“失败三次后验证”状态开启,其他场景读取各自开关。
function c(e, t) {
return s(e?.[`captcha_client_${t}`])
? !0
: t === `login` &&
s(e?.captcha_client_login_error) &&
s(e?.captcha_client_login_error_3_times);
}
/** 只解析 HTML 中的提供商标识,不执行其中脚本;未知插件或异常极验 id 明确报错。 */
function l(e) {
if (typeof e != `string` || e.length > 262144) throw Error(`验证码响应无效`);
let t = new DOMParser().parseFromString(e, `text/html`),
n = t.querySelector(`[data-captcha-provider]`);
if (n) {
let e = n.getAttribute(`data-captcha-id`) ?? ``;
if (
n.getAttribute(`data-captcha-provider`) !== `geetest` ||
!/^[a-f0-9]{32}$/i.test(e)
)
throw Error(`验证码服务尚未配置完成,请联系管理员`);
return {
provider: `geetest`,
captchaId: e,
};
}
if (
e.includes(`/captcha/tp_captcha/index/refresh`) &&
t.querySelector(`#captcha-img`)
)
return {
provider: `image`,
captchaId: ``,
};
throw Error(`当前验证码插件暂不受支持,请联系管理员`);
}
// 极验四个结果字段均需非空且有长度上限;真实挑战仍由服务端校验。
function u(e) {
if (!e || typeof e != `object`) return !1;
let t = e;
return [`lot_number`, `captcha_output`, `pass_token`, `gen_time`].every(
(e) =>
typeof t[e] == `string` && t[e].trim().length > 0 && t[e].length <= 8192,
);
}
/** d 为场景名;无场景时由调用方手动控制加载,组件销毁会取消请求并清空凭证。 */
function d(d) {
let f = useSiteConfig_t(),
p = useApi(),
m = ref(!1),
h = ref(`image`),
g = ref(``),
_ = ref(``),
v = ref(``),
y = ref(``),
b = ref(!1),
x = ref(``),
S = ref(!(!d || f.config.value)),
C = !1,
w = 0,
T = null,
E = !1;
// 每次加载或失效都清掉旧答案和 token,避免把不同验证码的结果混在一起提交。
function D() {
((y.value = ``), (v.value = ``));
}
// 序号 w 和销毁标记 C 防止旧响应回填;定时 AbortController 限制同源图片请求。
async function O() {
if (C || b.value) return;
let e = ++w;
((b.value = !0),
(x.value = ``),
D(),
(_.value = ``),
(g.value = ``),
(T = new AbortController()));
let t = T,
n = setTimeout(() => t.abort(), 1e4);
try {
let n = await p.get(`/captcha`, void 0, !1, {
timeoutMs: 1e4,
});
if (C || e !== w) return;
let r = l(n.html);
if (
((h.value = r.provider),
(g.value = r.captchaId),
r.provider === `geetest`)
)
return;
let i = await fetch(`/captcha/tp_captcha/index/refresh`, {
headers: {
Accept: `application/json`,
},
signal: t.signal,
cache: `no-store`,
}),
a = await i.json();
if (C || e !== w) return;
if (
!i.ok ||
typeof a?.base64 != `string` ||
!/^data:image\/(?:png|jpeg|gif|webp);base64,/.test(a.base64) ||
typeof a?.token != `string` ||
!a.token
)
throw Error(`验证码加载失败`);
((_.value = a.base64), (v.value = a.token));
} catch (t) {
if (C || e !== w) return;
x.value =
t instanceof Error && /验证码.*(?:配置|支持)/.test(t.message)
? t.message
: `验证码加载失败,请点击重新加载`;
} finally {
(clearTimeout(n), !C && e === w && (b.value = !1));
}
}
// 仅在当前确实需要极验且没有加载时接受完成结果,按后端约定序列化四个字段。
function k(e) {
(D(),
!C &&
m.value &&
h.value === `geetest` &&
!b.value &&
u(e) &&
((y.value = JSON.stringify({
lot_number: e.lot_number,
captcha_output: e.captcha_output,
pass_token: e.pass_token,
gen_time: e.gen_time,
})),
(v.value = e.lot_number)));
}
// 提交前将“配置未加载”“正在加载”“未完成”等情况区分,空串才表示可继续。
function A() {
return S.value
? `验证码设置正在加载,请稍候`
: d && !f.config.value
? `验证码设置加载失败,请刷新页面重试`
: m.value
? b.value
? `验证码正在加载,请稍候`
: x.value
? x.value
: h.value === `geetest`
? v.value && y.value
? ``
: `请先完成人机验证`
: v.value
? y.value.trim()
? ``
: `请输入图形验证码`
: `请先重新加载图形验证码`
: ``;
}
// 后端报错可临时强制开启验证码,刷新后再由页面让用户重试原动作,不自动重复登录。
async function j(e) {
let t = e instanceof ApiError && s(e.data?.captcha);
return (t && ((E = !0), (m.value = !0)), m.value && (await O()), t);
}
return (
d &&
(watch(
() => c(f.config.value, d),
(e) => {
((m.value = E || e),
m.value
? O()
: ((w += 1), T?.abort(), (b.value = !1), (x.value = ``), D()));
},
{
immediate: !0,
},
),
onMounted(async () => {
(await f.load(),
!C &&
((S.value = !1),
f.config.value ||
(x.value = `验证码设置加载失败,请刷新页面重试`)));
})),
// 组件离开后即使不可取消的 API 请求返回,也不能继续写入当前验证码状态。
onScopeDispose(() => {
((C = !0), (w += 1), T?.abort(), D());
}),
{
needed: m,
provider: h,
captchaId: g,
img: _,
token: v,
value: y,
loading: b,
error: x,
configLoading: S,
load: O,
complete: k,
invalidate: D,
validationError: A,
handleError: j,
}
);
}
export { d as t };