115 lines
4.4 KiB
JavaScript
115 lines
4.4 KiB
JavaScript
export const AUTH_TAC_SCENE = Object.freeze({
|
|
SMS_LOGIN: "APP_SMS_LOGIN",
|
|
REGISTER: "APP_REGISTER",
|
|
FORGOT_PASSWORD: "APP_FORGOT_PASSWORD",
|
|
});
|
|
|
|
export const PASSWORD_TAC_BLOCKED_MESSAGE =
|
|
"密码登录的服务端安全验证尚未开放,请先使用验证码登录";
|
|
|
|
const SUPPORTED_TAC_TYPES = new Set([
|
|
"SLIDER",
|
|
"ROTATE",
|
|
"CONCAT",
|
|
"WORD_IMAGE_CLICK",
|
|
]);
|
|
const PHONE_PATTERN = /^1[3-9]\d{9}$/;
|
|
|
|
export const isAuthPhone = (value) =>
|
|
typeof value === "string" && PHONE_PATTERN.test(value);
|
|
|
|
const contractError = (message, code) => {
|
|
const error = new Error(message);
|
|
error.code = code;
|
|
return error;
|
|
};
|
|
|
|
const requireText = (value, label) => {
|
|
if (typeof value !== "string" || !value.trim()) {
|
|
throw contractError(`${label}不能为空`, "AUTH_TAC_CONTRACT_INVALID");
|
|
}
|
|
return value.trim();
|
|
};
|
|
|
|
export const assertSmsCode = (value) => {
|
|
if (typeof value !== "string" || !/^\d{4}$/.test(value)) {
|
|
throw contractError("请输入 4 位短信验证码", "SMS_CODE_INVALID");
|
|
}
|
|
return value;
|
|
};
|
|
|
|
export const normalizeCaptchaRequirement = (value, expectedSceneCode) => {
|
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
throw contractError("行为验证策略响应无效", "AUTH_TAC_REQUIREMENT_INVALID");
|
|
}
|
|
const sceneCode = requireText(value.sceneCode, "行为验证场景");
|
|
if (sceneCode !== expectedSceneCode) {
|
|
throw contractError("行为验证场景与当前操作不匹配", "AUTH_TAC_SCENE_MISMATCH");
|
|
}
|
|
// 短信接口把 validToken 定义为必填,因此 required=false 不能在客户端被解释成
|
|
// “跳过验证”。后端必须为短信场景启用 TAC,或另行签发可消费的免验证票据。
|
|
if (value.required !== true) {
|
|
throw contractError(
|
|
"服务端未要求行为验证,无法取得发送短信所需票据",
|
|
"AUTH_TAC_POLICY_INCOMPLETE",
|
|
);
|
|
}
|
|
const providerCode = requireText(value.providerCode, "行为验证服务商").toUpperCase();
|
|
if (providerCode !== "TIANAI") {
|
|
throw contractError("当前仅支持 TIANAI 行为验证服务", "AUTH_TAC_PROVIDER_UNSUPPORTED");
|
|
}
|
|
const captchaType = requireText(value.captchaType, "行为验证码类型").toUpperCase();
|
|
if (!SUPPORTED_TAC_TYPES.has(captchaType)) {
|
|
throw contractError("服务端返回了客户端不支持的验证码类型", "AUTH_TAC_TYPE_UNSUPPORTED");
|
|
}
|
|
const ttlSeconds = Number(value.ttlSeconds);
|
|
if (!Number.isInteger(ttlSeconds) || ttlSeconds < 1) {
|
|
throw contractError("行为验证策略缺少有效期", "AUTH_TAC_TTL_INVALID");
|
|
}
|
|
return { required: true, providerCode, captchaType, sceneCode, ttlSeconds };
|
|
};
|
|
|
|
export const createTacRenderContext = ({
|
|
requestId,
|
|
baseUrl,
|
|
clientId,
|
|
tenantId,
|
|
sceneCode,
|
|
subject,
|
|
requirement,
|
|
}) => {
|
|
const normalizedRequirement = normalizeCaptchaRequirement(requirement, sceneCode);
|
|
const normalizedBaseUrl = requireText(baseUrl, "后端地址").replace(/\/+$/, "");
|
|
if (!/^https:\/\/[^/]+/i.test(normalizedBaseUrl)) {
|
|
throw contractError("行为验证只允许使用 HTTPS 后端地址", "AUTH_TAC_HTTPS_REQUIRED");
|
|
}
|
|
const normalizedSubject = requireText(subject, "手机号");
|
|
if (!isAuthPhone(normalizedSubject)) {
|
|
throw contractError("请输入正确的手机号", "AUTH_PHONE_INVALID");
|
|
}
|
|
return {
|
|
requestId: requireText(requestId, "验证请求标识"),
|
|
baseUrl: normalizedBaseUrl,
|
|
challengeUrl: `${normalizedBaseUrl}/captcha/challenge`,
|
|
verifyUrl: `${normalizedBaseUrl}/captcha/verify`,
|
|
clientId: requireText(clientId, "客户端标识"),
|
|
tenantId: requireText(tenantId, "租户标识"),
|
|
sceneCode: normalizedRequirement.sceneCode,
|
|
subject: normalizedSubject,
|
|
providerCode: normalizedRequirement.providerCode,
|
|
captchaType: normalizedRequirement.captchaType,
|
|
};
|
|
};
|
|
|
|
export const normalizeTacSuccess = (value, expectedRequestId) => {
|
|
if (!value || typeof value !== "object" || value.requestId !== expectedRequestId) {
|
|
throw contractError("行为验证结果已过期或与当前请求不匹配", "AUTH_TAC_RESULT_STALE");
|
|
}
|
|
const validToken = requireText(value.validToken, "行为验证票据");
|
|
const expireSeconds = Number(value.expireSeconds);
|
|
if (!Number.isInteger(expireSeconds) || expireSeconds < 1) {
|
|
throw contractError("行为验证票据缺少有效期", "AUTH_TAC_RESULT_INVALID");
|
|
}
|
|
return { requestId: value.requestId, validToken, expireSeconds };
|
|
};
|