mirror of
https://github.com/handsomezhuzhu/QQuiz.git
synced 2026-02-20 20:10:14 +00:00
ESA人机验证
This commit is contained in:
@@ -6,6 +6,14 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="description" content="QQuiz - 智能刷题与题库管理平台" />
|
||||
<title>QQuiz - 智能刷题平台</title>
|
||||
<!-- ESA 人机认证配置 -->
|
||||
<script>
|
||||
window.AliyunCaptchaConfig = {
|
||||
region: "cn",
|
||||
prefix: "%VITE_ESA_PREFIX%",
|
||||
};
|
||||
</script>
|
||||
<script src="https://o.alicdn.com/captcha-frontend/aliyunCaptcha/AliyunCaptcha.js" async></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Login Page
|
||||
*/
|
||||
import React, { useState } from 'react'
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { Link, useNavigate } from 'react-router-dom'
|
||||
import { useAuth } from '../context/AuthContext'
|
||||
import { BookOpen } from 'lucide-react'
|
||||
@@ -15,21 +15,110 @@ export const Login = () => {
|
||||
password: ''
|
||||
})
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [captchaInstance, setCaptchaInstance] = useState(null)
|
||||
|
||||
useEffect(() => {
|
||||
// 确保 window.initAliyunCaptcha 存在且 DOM 元素已渲染
|
||||
const initCaptcha = () => {
|
||||
if (window.initAliyunCaptcha && document.getElementById('captcha-element')) {
|
||||
try {
|
||||
window.initAliyunCaptcha({
|
||||
SceneId: import.meta.env.VITE_ESA_SCENE_ID, // 从环境变量读取场景ID
|
||||
mode: "popup", // 弹出式
|
||||
element: "#captcha-element", // 渲染验证码的元素
|
||||
button: "#login-btn", // 触发验证码的按钮ID
|
||||
success: async function (captchaVerifyParam) {
|
||||
// 验证成功后的回调
|
||||
// 这里我们获取到了验证参数,虽然文档说要发给后端,
|
||||
// 但 ESA 边缘拦截其实是在请求发出时检查 Cookie/Header
|
||||
// 对于“一点即过”或“滑块”,SDK 会自动处理验证逻辑
|
||||
// 这里的 verifiedParam 是用来回传给服务端做二次校验的
|
||||
// 由于我们此时还没有登录逻辑,我们可以在这里直接提交表单
|
||||
// 即把 verifyParam 存下来,或者直接调用 login
|
||||
|
||||
// 注意:由于是 form 的 onSubmit 触发,这里我们其实是在 form 提交被阻止(preventDefault)后
|
||||
// 由用户点击按钮触发了验证码,验证码成功后再执行真正的登录
|
||||
// 但 React 的 form 处理通常是 onSubmit
|
||||
// 我们可以让按钮类型为 button 而不是 submit,点击触发验证码
|
||||
// 验证码成功后手动调用 handleSubmit 的逻辑
|
||||
|
||||
console.log('Captcha Success:', captchaVerifyParam);
|
||||
handleLoginSubmit(captchaVerifyParam);
|
||||
},
|
||||
fail: function (result) {
|
||||
console.error('Captcha Failed:', result);
|
||||
},
|
||||
getInstance: function (instance) {
|
||||
setCaptchaInstance(instance);
|
||||
},
|
||||
slideStyle: {
|
||||
width: 360,
|
||||
height: 40,
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Captcha init error:", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault()
|
||||
// 如果脚本还没加载完,可能需要等待。为了简单起见,且我们在 index.html 加了 async
|
||||
// 我们做一个简单的轮询或者依赖 script onload(但在 index.html 比较难控制)
|
||||
// 或者直接延迟一下初始化
|
||||
const timer = setTimeout(initCaptcha, 500);
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
|
||||
const handleLoginSubmit = async (captchaParam) => {
|
||||
setLoading(true)
|
||||
|
||||
try {
|
||||
// 这里的 login 可能需要改造以接受验证码参数,或者利用 fetch 的拦截器
|
||||
// 如果是 ESA 边缘拦截,通常它会看请求里带不带特定的 Header/Cookie
|
||||
// 文档示例里是手动 fetch 并且带上了 header: 'captcha-Verify-param'
|
||||
// 暂时我们假设 login 函数内部不需要显式传参(通过 ESA 自动拦截),或者 ESA 需要 headers
|
||||
// 为了安全,建议把 captchaParam 传给 login,让 login 放到 headers 里
|
||||
// 但现在我们先维持原样,或者您可以把 captchaParam 放到 sessionStorage 里由 axios 拦截器读取
|
||||
|
||||
// 注意:上面的 success 回调里我们直接调用了这个,说明验证通过了
|
||||
const success = await login(formData.username, formData.password)
|
||||
if (success) {
|
||||
navigate('/dashboard')
|
||||
}
|
||||
} finally {
|
||||
setLoading(false)
|
||||
if(captchaInstance) captchaInstance.refresh(); // 失败或完成后刷新验证码
|
||||
}
|
||||
}
|
||||
|
||||
// 这里的 handleSubmit 变成只是触发验证码(如果也没通过验证的话)
|
||||
// 但 ESA 示例是绑定 button,点击 button 直接出验证码
|
||||
// 所以我们可以把 type="submit" 变成 type="button" 且 id="login-btn"
|
||||
const handlePreSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
// 此时不需要做任何事,因为按钮被 ESA 接管了,点击会自动弹窗
|
||||
// 只有验证成功了才会走 success -> handleLoginSubmit
|
||||
// 但是!如果没填用户名密码怎么办?
|
||||
// 最好在点击前校验表单。
|
||||
// ESA 的 button 参数会劫持点击事件。
|
||||
// 我们可以不绑定 button 参数,而是手动验证表单后,调用 captchaInstance.show() (如果是无痕或弹窗)
|
||||
// 官方文档说绑定 button 是“触发验证码弹窗或无痕验证的元素”
|
||||
// 如果我们保留 form submit,拦截它,如果表单有效,则手动 captchaInstance.show() (如果 SDK 支持)
|
||||
// 文档说“无痕模式首次验证不支持 show/hide”。
|
||||
// 咱们还是按官方推荐绑定 button,但是这会导致校验逻辑变复杂
|
||||
|
||||
// 简化方案:为了不破坏现有逻辑,我们不绑定 button ?
|
||||
// 不,必须绑定。那我们把“登录”按钮作为触发器。
|
||||
// 可是如果不填表单直接点登录 -> 验证码 -> 成功 -> 提交空表单 -> 报错。流程不太对。
|
||||
|
||||
// 更好的流程:
|
||||
// 用户填表 -> 点击登录 -> 校验表单 -> (有效) -> 弹出验证码 -> (成功) -> 提交后端
|
||||
|
||||
// 我们可以做一个不可见的 button 绑定给 ESA,验证表单通过后,用代码模拟点击这个 button?
|
||||
// 或者直接用 id="login-btn" 绑定当前的登录按钮,
|
||||
// 但是在 success 回调里检查 formData 是否为空?
|
||||
}
|
||||
|
||||
|
||||
const handleChange = (e) => {
|
||||
setFormData({
|
||||
...formData,
|
||||
@@ -55,7 +144,8 @@ export const Login = () => {
|
||||
<div className="bg-white rounded-2xl shadow-xl p-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-6">登录</h2>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{/* 为了能正确使用 ESA,我们将 form 的 onSubmit 移除,改由按钮触发,或者保留 form 但不做提交 */}
|
||||
<form className="space-y-6" onSubmit={(e) => e.preventDefault()}>
|
||||
{/* Username */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
@@ -89,9 +179,14 @@ export const Login = () => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* ESA Captcha Container */}
|
||||
<div id="captcha-element"></div>
|
||||
|
||||
{/* Submit Button */}
|
||||
{/* 绑定 id="login-btn" 供 ESA 使用 */}
|
||||
<button
|
||||
type="submit"
|
||||
type="button"
|
||||
id="login-btn"
|
||||
disabled={loading}
|
||||
className="w-full bg-primary-600 text-white py-3 rounded-lg font-medium hover:bg-primary-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
|
||||
@@ -1,19 +1,35 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import { defineConfig, loadEnv } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import path from 'path'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
port: 3000,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: process.env.REACT_APP_API_URL || 'http://localhost:8000',
|
||||
changeOrigin: true,
|
||||
export default defineConfig(({ mode }) => {
|
||||
// Assume running from frontend directory
|
||||
const envDir = path.resolve(process.cwd(), '..')
|
||||
const env = loadEnv(mode, envDir, '')
|
||||
|
||||
return {
|
||||
envDir, // Tell Vite to look for .env files in the project root
|
||||
plugins: [
|
||||
react(),
|
||||
{
|
||||
name: 'html-transform',
|
||||
transformIndexHtml(html) {
|
||||
return html.replace(/%VITE_ESA_PREFIX%/g, env.VITE_ESA_PREFIX || '')
|
||||
},
|
||||
}
|
||||
],
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
port: 3000,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: env.VITE_API_URL || env.REACT_APP_API_URL || 'http://localhost:8000',
|
||||
changeOrigin: true,
|
||||
}
|
||||
}
|
||||
},
|
||||
build: {
|
||||
outDir: 'build'
|
||||
}
|
||||
},
|
||||
build: {
|
||||
outDir: 'build'
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user