diff --git a/.env.example b/.env.example index 250549f..70e2275 100644 --- a/.env.example +++ b/.env.example @@ -52,3 +52,7 @@ CORS_ORIGINS=http://localhost:3000,http://127.0.0.1:3000 # Upload Directory UPLOAD_DIR=./uploads + +# ESA Human Verification +VITE_ESA_PREFIX= +VITE_ESA_SCENE_ID= diff --git a/frontend/index.html b/frontend/index.html index 4182b3c..2915a45 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -6,6 +6,14 @@ QQuiz - 智能刷题平台 + + +
diff --git a/frontend/src/pages/Login.jsx b/frontend/src/pages/Login.jsx index b59c6f7..b0e56e3 100644 --- a/frontend/src/pages/Login.jsx +++ b/frontend/src/pages/Login.jsx @@ -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 = () => {

登录

-
+ {/* 为了能正确使用 ESA,我们将 form 的 onSubmit 移除,改由按钮触发,或者保留 form 但不做提交 */} + e.preventDefault()}> {/* Username */}
+ {/* ESA Captcha Container */} +
+ {/* Submit Button */} + {/* 绑定 id="login-btn" 供 ESA 使用 */}