217 lines
6.6 KiB
JavaScript
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 };
|