修复认证:隔离实名认证内容并处理二维码图片失败
This commit is contained in:
1 parent
9b8124a4ea
commit
b3a66d97c8
3 files changed
+264
-5
No files matched your search
@@ -123,6 +123,7 @@ var fe = {
|
|||||||
(e.preventDefault(), (N.value = t[r]), F.value[N.value]?.focus());
|
(e.preventDefault(), (N.value = t[r]), F.value[N.value]?.focus());
|
||||||
}
|
}
|
||||||
let I = ref(null),
|
let I = ref(null),
|
||||||
|
imageReady = ref(!1),
|
||||||
L = ref(!1),
|
L = ref(!1),
|
||||||
R = ref(!1),
|
R = ref(!1),
|
||||||
z = ref(``),
|
z = ref(``),
|
||||||
@@ -158,6 +159,8 @@ var fe = {
|
|||||||
const attempt = C.prepareLogin();
|
const attempt = C.prepareLogin();
|
||||||
loginAttempt = attempt;
|
loginAttempt = attempt;
|
||||||
(Z(),
|
(Z(),
|
||||||
|
(I.value = null),
|
||||||
|
(imageReady.value = !1),
|
||||||
(z.value = ``),
|
(z.value = ``),
|
||||||
(R.value = !1),
|
(R.value = !1),
|
||||||
(B.value = []),
|
(B.value = []),
|
||||||
@@ -178,7 +181,7 @@ var fe = {
|
|||||||
!t.token
|
!t.token
|
||||||
)
|
)
|
||||||
throw Error(`二维码加载失败,请重新获取`);
|
throw Error(`二维码加载失败,请重新获取`);
|
||||||
((I.value = t), (z.value = ``), Ee());
|
((I.value = t), (z.value = ``));
|
||||||
} catch (t) {
|
} catch (t) {
|
||||||
if (e !== K || !attempt.isCurrent()) return;
|
if (e !== K || !attempt.isCurrent()) return;
|
||||||
(J(),
|
(J(),
|
||||||
@@ -188,6 +191,26 @@ var fe = {
|
|||||||
e === K && (L.value = !1);
|
e === K && (L.value = !1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 接口返回地址并不代表图片可扫码;只有当前图片解码成功才启动登录轮询。
|
||||||
|
function qrImageLoaded(generation) {
|
||||||
|
if (
|
||||||
|
generation !== K ||
|
||||||
|
L.value ||
|
||||||
|
imageReady.value ||
|
||||||
|
!I.value ||
|
||||||
|
!loginAttempt?.isCurrent()
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
((imageReady.value = !0), Ee());
|
||||||
|
}
|
||||||
|
function qrImageFailed(generation) {
|
||||||
|
if (generation !== K || !I.value || !loginAttempt?.isCurrent())
|
||||||
|
return;
|
||||||
|
// 图片失败废弃整个扫码代次,迟到的状态响应不能再登录或转入绑定。
|
||||||
|
(Q(),
|
||||||
|
(R.value = !0),
|
||||||
|
(z.value = `二维码图片加载失败,请刷新后重试`));
|
||||||
|
}
|
||||||
async function De() {
|
async function De() {
|
||||||
// 同一代次最多一个查询在途;Wait/Success/Expired/SelectClient/WaitBind 分别处理。
|
// 同一代次最多一个查询在途;Wait/Success/Expired/SelectClient/WaitBind 分别处理。
|
||||||
if (!I.value || q === K || !loginAttempt?.isCurrent()) return;
|
if (!I.value || q === K || !loginAttempt?.isCurrent()) return;
|
||||||
@@ -273,6 +296,7 @@ var fe = {
|
|||||||
(K++,
|
(K++,
|
||||||
Z(),
|
Z(),
|
||||||
(I.value = null),
|
(I.value = null),
|
||||||
|
(imageReady.value = !1),
|
||||||
(loginAttempt = null),
|
(loginAttempt = null),
|
||||||
(L.value = !1),
|
(L.value = !1),
|
||||||
(H.value = !1),
|
(H.value = !1),
|
||||||
@@ -345,7 +369,8 @@ var fe = {
|
|||||||
(n, r) => {
|
(n, r) => {
|
||||||
let a = radiobutton,
|
let a = radiobutton,
|
||||||
s = button,
|
s = button,
|
||||||
u = message;
|
u = message,
|
||||||
|
imageGeneration = K;
|
||||||
return unref(k)
|
return unref(k)
|
||||||
? (openBlock(),
|
? (openBlock(),
|
||||||
createElementBlock(`aside`, fe, [
|
createElementBlock(`aside`, fe, [
|
||||||
@@ -454,7 +479,9 @@ var fe = {
|
|||||||
"qr-box-idle": !unref(I),
|
"qr-box-idle": !unref(I),
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
"aria-busy": unref(L),
|
"aria-busy":
|
||||||
|
unref(L) ||
|
||||||
|
(!!unref(I) && !unref(imageReady)),
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
unref(I) && !unref(R)
|
unref(I) && !unref(R)
|
||||||
@@ -462,7 +489,7 @@ var fe = {
|
|||||||
createElementBlock(
|
createElementBlock(
|
||||||
`img`,
|
`img`,
|
||||||
{
|
{
|
||||||
key: 0,
|
key: imageGeneration,
|
||||||
class: normalizeClass([
|
class: normalizeClass([
|
||||||
`qr-img`,
|
`qr-img`,
|
||||||
{
|
{
|
||||||
@@ -471,6 +498,14 @@ var fe = {
|
|||||||
]),
|
]),
|
||||||
src: unref(I).img_url,
|
src: unref(I).img_url,
|
||||||
alt: `微信扫码登录`,
|
alt: `微信扫码登录`,
|
||||||
|
onLoad: () =>
|
||||||
|
qrImageLoaded(
|
||||||
|
imageGeneration,
|
||||||
|
),
|
||||||
|
onError: () =>
|
||||||
|
qrImageFailed(
|
||||||
|
imageGeneration,
|
||||||
|
),
|
||||||
},
|
},
|
||||||
null,
|
null,
|
||||||
10,
|
10,
|
||||||
@@ -535,7 +570,8 @@ var fe = {
|
|||||||
`p`,
|
`p`,
|
||||||
ye,
|
ye,
|
||||||
toDisplayString(
|
toDisplayString(
|
||||||
unref(L) && !unref(I)
|
unref(L) ||
|
||||||
|
(unref(I) && !unref(imageReady))
|
||||||
? `二维码加载中…`
|
? `二维码加载中…`
|
||||||
: unref(B).length
|
: unref(B).length
|
||||||
? `选择账号后继续登录`
|
? `选择账号后继续登录`
|
||||||
|
|||||||
@@ -2569,6 +2569,8 @@ var xe = {
|
|||||||
{
|
{
|
||||||
key: 1,
|
key: 1,
|
||||||
srcdoc: unref(X),
|
srcdoc: unref(X),
|
||||||
|
// 认证插件只拥有自己的渲染环境,不能继承控制台身份或访问父页面。
|
||||||
|
sandbox: `allow-scripts`,
|
||||||
class: `auth-frame`,
|
class: `auth-frame`,
|
||||||
title: `实名认证二维码`,
|
title: `实名认证二维码`,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,221 @@
|
|||||||
|
/**
|
||||||
|
* Failure modes recorded before the fixes:
|
||||||
|
* - Certification HTML must keep its QR-refresh script, without reading or writing the console parent.
|
||||||
|
* - Receiving a QR URL is not evidence that an image loaded; pending/broken images must not start login polling.
|
||||||
|
* - A broken image needs an accessible error and a retry, and a fresh loaded image must still permit normal login.
|
||||||
|
* All scenarios enter the actual built pages and replace only the external HTTP service or browser clock.
|
||||||
|
*/
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { test } from "node:test";
|
||||||
|
import {
|
||||||
|
appOrigin,
|
||||||
|
backendOrigin,
|
||||||
|
eventually,
|
||||||
|
login,
|
||||||
|
scenario,
|
||||||
|
} from "./helpers/console-browser.mjs";
|
||||||
|
|
||||||
|
const qrImage = "R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==";
|
||||||
|
|
||||||
|
function reply(route, data) {
|
||||||
|
return route.fulfill({
|
||||||
|
json: { status: 200, msg: "", data },
|
||||||
|
headers: { "Access-Control-Allow-Origin": "*" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test(
|
||||||
|
"certification QR refresh scripts run without console-parent privileges",
|
||||||
|
{ timeout: 30000 },
|
||||||
|
(t) =>
|
||||||
|
scenario(t, {}, async (page) => {
|
||||||
|
await page.addInitScript(() => {
|
||||||
|
document.addEventListener(
|
||||||
|
"DOMContentLoaded",
|
||||||
|
() => {
|
||||||
|
document.documentElement.dataset.auditIsolationSeed =
|
||||||
|
"synthetic-local-marker";
|
||||||
|
},
|
||||||
|
{ once: true },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await page
|
||||||
|
.context()
|
||||||
|
.route(`${backendOrigin}/console/v1/certification/**`, (route) => {
|
||||||
|
const path = new URL(route.request().url()).pathname;
|
||||||
|
if (path.endsWith("/info"))
|
||||||
|
return reply(route, {
|
||||||
|
certification_open: 1,
|
||||||
|
certification_company_open: 0,
|
||||||
|
certification_upload: 0,
|
||||||
|
is_certification: 0,
|
||||||
|
person: { status: 4 },
|
||||||
|
});
|
||||||
|
if (path.endsWith("/auth"))
|
||||||
|
return reply(route, {
|
||||||
|
html: `<!doctype html><html><body><div id="qrcode"><img src="data:image/gif;base64,${qrImage}"></div><script>
|
||||||
|
document.documentElement.dataset.auditPluginScript = "ran";
|
||||||
|
try {
|
||||||
|
parent.document.documentElement.dataset.auditFromCertification =
|
||||||
|
parent.document.documentElement.dataset.auditIsolationSeed;
|
||||||
|
document.documentElement.dataset.auditParentAccess = "allowed";
|
||||||
|
} catch {
|
||||||
|
document.documentElement.dataset.auditParentAccess = "blocked";
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
const replacement = document.createElement("img");
|
||||||
|
replacement.src = "data:image/gif;base64,${qrImage}";
|
||||||
|
replacement.dataset.auditDynamicQr = "updated";
|
||||||
|
document.querySelector("#qrcode").replaceChildren(replacement);
|
||||||
|
}, 30);
|
||||||
|
</script></body></html>`,
|
||||||
|
});
|
||||||
|
if (path.endsWith("/status"))
|
||||||
|
return reply(route, { code: 2, refresh: 0 });
|
||||||
|
return route.fallback();
|
||||||
|
});
|
||||||
|
await login(page, "/account/realname");
|
||||||
|
await page
|
||||||
|
.getByRole("button", { name: "继续扫码核验", exact: true })
|
||||||
|
.click();
|
||||||
|
const element = await page.locator("iframe.auth-frame").elementHandle();
|
||||||
|
const frame = await element.contentFrame();
|
||||||
|
await frame
|
||||||
|
.locator('[data-certification-qr][data-audit-dynamic-qr="updated"]')
|
||||||
|
.waitFor();
|
||||||
|
const observed = {
|
||||||
|
refreshedQrVisible: await frame
|
||||||
|
.locator("[data-certification-qr]")
|
||||||
|
.isVisible(),
|
||||||
|
pluginScriptRan: await frame.evaluate(
|
||||||
|
() => document.documentElement.dataset.auditPluginScript === "ran",
|
||||||
|
),
|
||||||
|
parentAccessBlocked: await frame.evaluate(
|
||||||
|
() =>
|
||||||
|
document.documentElement.dataset.auditParentAccess === "blocked",
|
||||||
|
),
|
||||||
|
parentMarkerUntouched: await page.evaluate(
|
||||||
|
() =>
|
||||||
|
document.documentElement.dataset.auditFromCertification ===
|
||||||
|
undefined,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
assert.equal(observed.refreshedQrVisible, true);
|
||||||
|
assert.equal(observed.pluginScriptRan, true);
|
||||||
|
assert.equal(observed.parentAccessBlocked, true);
|
||||||
|
assert.equal(observed.parentMarkerUntouched, true);
|
||||||
|
return observed;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"a broken WeChat image stops its scan and a fresh image recovers login",
|
||||||
|
{ timeout: 35000 },
|
||||||
|
(t) =>
|
||||||
|
scenario(t, {}, async (page) => {
|
||||||
|
await page.clock.install();
|
||||||
|
let acquisitions = 0;
|
||||||
|
let firstScanPolls = 0;
|
||||||
|
let secondScanPolls = 0;
|
||||||
|
let releaseImage;
|
||||||
|
const imageRelease = new Promise((resolve) => {
|
||||||
|
releaseImage = resolve;
|
||||||
|
});
|
||||||
|
let imageRequested;
|
||||||
|
const imageStarted = new Promise((resolve) => {
|
||||||
|
imageRequested = resolve;
|
||||||
|
});
|
||||||
|
await page.context().route(`${backendOrigin}/**`, async (route) => {
|
||||||
|
const url = new URL(route.request().url());
|
||||||
|
if (url.pathname === "/audit-qr/1.gif") {
|
||||||
|
imageRequested();
|
||||||
|
await imageRelease;
|
||||||
|
return route.fulfill({
|
||||||
|
contentType: "image/gif",
|
||||||
|
body: "invalid-image",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (url.pathname === "/audit-qr/2.gif")
|
||||||
|
return route.fulfill({
|
||||||
|
contentType: "image/gif",
|
||||||
|
body: Buffer.from(qrImage, "base64"),
|
||||||
|
});
|
||||||
|
if (url.pathname === "/console/v1/common")
|
||||||
|
return reply(route, {
|
||||||
|
website_name: "本地扫码验收",
|
||||||
|
login_phone_password: 1,
|
||||||
|
login_email_password: 1,
|
||||||
|
plugin_configuration: { mp_weixin_notice: { scan_login: 1 } },
|
||||||
|
});
|
||||||
|
if (url.pathname === "/console/v1/mp_weixin_notice/qrcode") {
|
||||||
|
acquisitions++;
|
||||||
|
return reply(route, {
|
||||||
|
img_url: `${backendOrigin}/audit-qr/${acquisitions}.gif`,
|
||||||
|
ticket: `local-qr-ticket-${acquisitions}`,
|
||||||
|
token: "local-qr-token",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (url.pathname === "/console/v1/mp_weixin_notice/qrcode/status") {
|
||||||
|
if (url.searchParams.get("ticket") === "local-qr-ticket-1") {
|
||||||
|
firstScanPolls++;
|
||||||
|
return reply(route, { status: "Wait" });
|
||||||
|
}
|
||||||
|
secondScanPolls++;
|
||||||
|
return reply(route, { status: "Success", jwt: "e2e-session-token" });
|
||||||
|
}
|
||||||
|
return route.fallback();
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await page.goto(`${appOrigin}/login?redirect=/security/ssh-keys`);
|
||||||
|
await page
|
||||||
|
.getByRole("button", { name: "获取二维码", exact: true })
|
||||||
|
.click();
|
||||||
|
await imageStarted;
|
||||||
|
await page.clock.runFor(5000);
|
||||||
|
const pollsBeforeImage = firstScanPolls;
|
||||||
|
releaseImage();
|
||||||
|
await eventually(
|
||||||
|
() =>
|
||||||
|
page.evaluate(() => {
|
||||||
|
const image = document.querySelector(".qr-img");
|
||||||
|
return !image || image.complete;
|
||||||
|
}),
|
||||||
|
Boolean,
|
||||||
|
"The invalid image must finish decoding",
|
||||||
|
);
|
||||||
|
await page.clock.runFor(5000);
|
||||||
|
const failureState = {
|
||||||
|
accessibleError: await page
|
||||||
|
.locator(".qr-status")
|
||||||
|
.getByRole("alert")
|
||||||
|
.isVisible(),
|
||||||
|
brokenImageRemoved: (await page.locator(".qr-img").count()) === 0,
|
||||||
|
pollsBeforeImage,
|
||||||
|
pollsAfterImageFailure: firstScanPolls,
|
||||||
|
};
|
||||||
|
await page
|
||||||
|
.getByRole("button", { name: "刷新二维码", exact: true })
|
||||||
|
.click();
|
||||||
|
await eventually(
|
||||||
|
() =>
|
||||||
|
page.evaluate(() => {
|
||||||
|
const image = document.querySelector(".qr-img");
|
||||||
|
return Boolean(image?.complete && image.naturalWidth > 0);
|
||||||
|
}),
|
||||||
|
Boolean,
|
||||||
|
"Retry must display a real decoded QR image",
|
||||||
|
);
|
||||||
|
await page.clock.runFor(2100);
|
||||||
|
await page.waitForURL((url) => url.pathname === "/security/ssh-keys");
|
||||||
|
await page.getByText("audit-01", { exact: true }).waitFor();
|
||||||
|
assert.equal(failureState.accessibleError, true);
|
||||||
|
assert.equal(failureState.brokenImageRemoved, true);
|
||||||
|
assert.equal(failureState.pollsBeforeImage, 0);
|
||||||
|
assert.equal(failureState.pollsAfterImageFailure, 0);
|
||||||
|
assert.equal(secondScanPolls, 1);
|
||||||
|
return { ...failureState, retryCompletedNormalLogin: true };
|
||||||
|
} finally {
|
||||||
|
releaseImage();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
Reference in new issue
Block a user