🌱 继续优化验证码登录,尝试引入geetest

This commit is contained in:
目棃
2024-07-27 12:51:28 +08:00
parent a8b1ff4588
commit 8f116c954f
11 changed files with 385 additions and 313 deletions

View File

@@ -9,7 +9,7 @@
</div>
</template>
<template #append>
<v-icon @click="confirmCUD" style="cursor: pointer" title="修改用户数据目录"
<v-icon @click="confirmCUD()" style="cursor: pointer" title="修改用户数据目录"
>mdi-pencil
</v-icon>
&emsp;
@@ -45,7 +45,9 @@
</div>
</template>
<template #append>
<v-icon @click="confirmCLD" style="cursor: pointer" title="清理日志文件">mdi-delete</v-icon>
<v-icon @click="confirmCLD()" style="cursor: pointer" title="清理日志文件"
>mdi-delete</v-icon
>
&emsp;
<v-icon @click="openPath('log')" style="cursor: pointer" title="打开日志目录"
>mdi-folder-open

View File

@@ -0,0 +1,59 @@
/**
* @file component/func/geetest.ts
* @description 封装自定义 geetest 组件,通过函数调用的方式,简化 geetest 的使用
* @since Beta v0.5.1
*/
import { h, render } from "vue";
import type { ComponentInternalInstance, VNode } from "vue";
import geetest from "./geetest.vue";
const geetestId = "tg-func-geetest";
/**
* @description 自定义 geetest 组件
* @since Beta v0.5.1
* @extends ComponentInternalInstance
* @property {Function} exposeProxy.displayBox 弹出 geetest 验证
* @return GeetestInstance
*/
interface GeetestInstance extends ComponentInternalInstance {
exposeProxy: {
displayBox: (
props: TGApp.Plugins.Mys.Geetest.reqResp,
) => Promise<TGApp.Plugins.Mys.Geetest.validateResp | false>;
};
}
const renderBox = (props: TGApp.Plugins.Mys.Geetest.reqResp): VNode => {
const container = document.createElement("div");
container.id = geetestId;
const boxVNode: VNode = h(geetest, props);
render(boxVNode, container);
document.body.appendChild(container);
return boxVNode;
};
let geetestInstance: VNode;
/**
* @function showGeetest
* @since Beta v0.5.1
* @description 弹出 geetest 验证
* @param {TGApp.Plugins.Mys.Geetest.reqResp} props geetest 验证的参数
* @return {Promise<TGApp.Plugins.Mys.Geetest.validateResp>} 验证成功返回验证数据
*/
async function showGeetest(
props: TGApp.Plugins.Mys.Geetest.reqResp,
): Promise<TGApp.Plugins.Mys.Geetest.validateResp | false> {
if (geetestInstance !== undefined) {
const boxVue = <GeetestInstance>geetestInstance.component;
return boxVue.exposeProxy.displayBox(props);
} else {
geetestInstance = renderBox(props);
return await showGeetest(props);
}
}
export default showGeetest;

View File

@@ -0,0 +1,182 @@
<template>
<transition name="func-geetest-outer">
<div v-show="show || showOuter" class="geetest-overlay" @click.self.prevent>
<transition name="func-geetest-inner">
<div v-show="showInner" class="geetest-box">
<div class="geetest-top">
<div class="geetest-title">请完成如下极验测试</div>
</div>
<div id="verify" class="geetest-mid">
<div id="geetest" ref="geetestRef"></div>
</div>
</div>
</transition>
</div>
</transition>
</template>
<script setup lang="ts">
import { onMounted, ref, watch } from "vue";
interface GeetestProps {
gt: string;
challenge: string;
new_captcha: number;
success: number;
}
const props = withDefaults(defineProps<GeetestProps>(), {
gt: "",
challenge: "",
new_captcha: 0,
success: 0,
});
const show = ref<boolean>(false);
const showOuter = ref<boolean>(false);
const showInner = ref<boolean>(false);
const geetestRef = ref<HTMLElement>(<HTMLElement>document.getElementById("geetest"));
watch(show, () => {
if (show.value) {
showOuter.value = true;
setTimeout(() => {
showInner.value = true;
}, 100);
} else {
setTimeout(() => {
showInner.value = false;
}, 100);
setTimeout(() => {
showOuter.value = false;
}, 300);
}
});
onMounted(async () => {
await displayBox(props);
});
async function displayBox(
props: TGApp.Plugins.Mys.Geetest.reqResp,
): Promise<TGApp.Plugins.Mys.Geetest.validateResp | false> {
if (!props.gt || !props.challenge) return false;
show.value = true;
return await new Promise<TGApp.Plugins.Mys.Geetest.validateResp>((resolve) => {
// eslint-disable-next-line no-undef
initGeetest(
{
gt: props.gt,
challenge: props.challenge,
offline: false,
new_captcha: true,
product: "custom",
area: "#verify",
width: "250px",
},
(captchaObj: TGApp.Plugins.Mys.Geetest.GeetestCaptcha) => {
geetestRef.value.innerHTML = "";
captchaObj.appendTo("#geetest");
captchaObj.onSuccess(async () => {
const validate = captchaObj.getValidate();
resolve(validate);
captchaObj.onClose(() => {
show.value = false;
});
});
},
);
});
}
defineExpose({
displayBox,
});
</script>
<style lang="css" scoped>
.func-geetest-outer-enter-active,
.func-geetest-outer-leave-active,
.func-geetest-inner-enter-active {
transition: all 0.3s;
}
.func-geetest-inner-leave-active {
transition: all 0.5s ease-in-out;
}
.func-geetest-inner-enter-from {
opacity: 0;
transform: scale(1.5);
}
.func-geetest-inner-enter-to,
.func-geetest-inner-leave-from {
opacity: 1;
transform: scale(1);
}
.func-geetest-outer-enter-to,
.func-geetest-outer-leave-from {
opacity: 1;
}
.func-geetest-outer-enter-from,
.func-geetest-outer-leave-to {
opacity: 0;
}
.func-geetest-inner-leave-to {
opacity: 0;
transform: scale(0);
}
.geetest-overlay {
position: fixed;
z-index: 100;
top: 0;
left: 0;
display: flex;
width: 100%;
height: 100%;
align-items: center;
justify-content: center;
backdrop-filter: blur(20px);
background: rgb(0 0 0 / 50%);
}
.geetest-box {
display: flex;
flex-direction: column;
padding: 10px;
border-radius: 5px;
background-color: var(--box-bg-1);
color: var(--app-page-content);
gap: 10px;
}
.geetest-top {
border-bottom: 1px solid var(--common-shadow-4);
font-family: var(--font-title);
text-align: center;
}
.geetest-title {
color: var(--common-text-title);
font-size: 20px;
}
.geetest-mid {
display: flex;
width: 100%;
align-items: flex-start;
justify-content: center;
padding: 10px;
border-radius: 5px;
background: var(--box-bg-2);
}
#verify {
width: 256px;
height: 320px;
}
</style>

View File

@@ -10,6 +10,7 @@ import { createVuetify } from "vuetify";
import App from "./App.vue";
import router from "./router/index.js";
import store from "./store/index.js";
import "https://static.geetest.com/static/js/gt.0.4.9.js";
import "@mdi/font/css/materialdesignicons.css";
import "vuetify/styles";

View File

@@ -23,6 +23,7 @@
</template>
<script lang="ts" setup>
import showConfirm from "../../components/func/confirm.js";
import showGeetest from "../../components/func/geetest.js";
import showSnackbar from "../../components/func/snackbar.js";
import Mys from "../../plugins/Mys/index.js";
@@ -33,16 +34,8 @@ async function tryCaptchaLogin(): Promise<void> {
text: "+86",
});
if (!phone) return;
const captchaResp = await Mys.User.getCaptcha(phone);
console.log("[captchaResp]", captchaResp);
if ("retcode" in captchaResp) {
showSnackbar({
text: `[${captchaResp.retcode}] ${captchaResp.message}`,
color: "error",
});
return;
}
const action_type = captchaResp.action_type;
const action_type = await tryGetCaptcha(phone);
if (!action_type) return;
const captcha = await showConfirm({
mode: "input",
title: "请输入验证码",
@@ -52,6 +45,25 @@ async function tryCaptchaLogin(): Promise<void> {
const loginResp = await Mys.User.login(phone, captcha, action_type);
console.log("[loginResp]", loginResp);
}
async function tryGetCaptcha(phone: string, aigis?: string): Promise<string | false> {
const captchaResp = await Mys.User.getCaptcha(phone, aigis);
console.log("[captchaResp]", captchaResp);
if ("retcode" in captchaResp) {
if (!captchaResp.data || captchaResp.data === "") {
showSnackbar({
text: `[${captchaResp.retcode}] ${captchaResp.message}`,
color: "error",
});
return false;
}
const aigis: TGApp.Plugins.Mys.CaptchaLogin.CaptchaAigis = JSON.parse(captchaResp.data);
const resp = await showGeetest(aigis.data);
const aigisStr = btoa(`${aigis.session_id};${JSON.stringify(resp)}`);
return await tryGetCaptcha(phone, aigisStr);
}
return captchaResp.action_type;
}
</script>
<style lang="css" scoped>
.test-box {

View File

@@ -40,12 +40,13 @@ function rsaEncrypt(data: string): string {
/**
* @description 获取短信验证码
* @since Beta v0.5.1
* @todo retcode 为-3101时表示需要进行验证需要从resp.headers["x-rpc-aigis"]中获取相关数据
* @param {string} phone - 手机号
* @param {string} [aigis] - 验证数据
* @returns {Promise<TGApp.Plugins.Mys.CaptchaLogin.CaptchaData | TGApp.BBS.Response.Base>}
*/
export async function getCaptcha(
phone: string,
aigis?: string,
): Promise<TGApp.Plugins.Mys.CaptchaLogin.CaptchaData | TGApp.BBS.Response.BaseWithData> {
const url = "https://passport-api.mihoyo.com/account/ma-cn-verifier/verifier/createLoginCaptcha";
const device_fp = getDeviceInfo("device_fp");
@@ -54,7 +55,7 @@ export async function getCaptcha(
const device_model = getDeviceInfo("product");
const body = { area_code: rsaEncrypt("+86"), mobile: rsaEncrypt(phone) };
const header: Record<string, string> = {
"x-rpc-aigis": "",
"x-rpc-aigis": aigis || "",
"x-rpc-app_version": TGConstant.BBS.VERSION,
"x-rpc-client_type": "2",
"x-rpc-app_id": TGConstant.BBS.APP_ID,

View File

@@ -38,6 +38,21 @@ declare namespace TGApp.Plugins.Mys.CaptchaLogin {
action_type: string;
}
/**
* @description 触发验证的序列化数据
* @since Beta v0.5.1
* @interface CaptchaAigis
* @property {string} session_id 会话 id
* @property {number} mmt_type mmt 类型
* @property {TGApp.Plugins.Mys.Geetest.getData} data 数据
* @return CaptchaBody
*/
interface CaptchaAigis {
session_id: string;
mmt_type: number;
data: TGApp.Plugins.Mys.Geetest.reqResp;
}
/**
* @description 短信验证码登录返回数据
* @since Beta v0.5.1

98
src/plugins/Mys/types/Geetest.d.ts vendored Normal file
View File

@@ -0,0 +1,98 @@
/**
* @file plugins/Mys/types/Geetest.d.ts
* @description Mys 插件 Geetest 类型定义文件
* @since Beta v0.5.1
*/
/**
* @description Mys 插件 Geetest 类型
* @since Beta v0.5.1
* @namespace TGApp.Plugins.Mys.Geetest
* @memberof TGApp.Plugins.Mys
*/
declare namespace TGApp.Plugins.Mys.Geetest {
/**
* @description 极验验证的响应数据
* @since Beta v0.5.1
* @interface reqResp
* @property {string} gt - 极验验证 gt
* @property {string} challenge - 极验验证 challenge
* @property {number} new_captcha - 极验验证 new_captcha
* @property {number} success - 极验验证 success
* @return reqResp
*/
interface reqResp {
gt: string;
challenge: string;
new_captcha: number;
success: number;
}
/**
* @description 极验验证的请求数据
* @since Beta v0.5.1
* @interface postData
* @property {string} challenge - 极验验证 challenge
* @property {string} validate - 极验验证 validate
* @return postData
*/
interface postData {
challenge: string;
validate: string;
}
/**
* @description 极验验证的请求方法-请求参数
* @since Beta v0.5.1
* @interface InitGeetestParams
* @property {string} gt - 极验验证 gt
* @property {string} challenge - 极验验证 challenge
* @property {boolean} offline - 极验验证 offline
* @property {boolean} new_captcha - 极验验证 new_captcha
* @property {string} product - 极验验证 product
* @property {string} width - 极验验证 width
* @property {string} area - 极验验证 area
* @return InitGeetestParams
*/
interface InitGeetestParams {
gt: string;
challenge: string;
offline: boolean;
new_captcha: boolean;
product: string;
width: string;
area: string;
}
/**
* @description Geetest 插件 captchaObj
* @since Beta v0.5.1
* @interface GeetestCaptcha
* @property {Function} appendTo
* @property {Function} getValidate
* @property {Function} onSuccess
* @property {Function} onClose
* @return GeetestCaptcha
*/
interface GeetestCaptcha {
appendTo: (selector: string) => void;
getValidate: () => validateResp;
onSuccess: (callback: () => void) => void;
onClose: (callback: () => void) => void;
}
/**
* @description Geetest 插件 validate
* @since Beta v0.5.1
* @interface validateResp
* @property {string} geetest_challenge
* @property {string} geetest_validate
* @property {string} geetest_seccode
* @return validateResp
*/
interface validateResp {
geetest_challenge: string;
geetest_validate: string;
geetest_seccode: string;
}
}