From fb73a69ecdc11395dec11d1c13052fe5ab059906 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Tue, 21 Jul 2026 11:36:49 +0800 Subject: [PATCH] =?UTF-8?q?feat(web):=20#558=20=E5=BE=AE=E4=BF=A1=E7=99=BB?= =?UTF-8?q?=E5=BD=95=E4=BD=93=E9=AA=8C=E4=BC=98=E5=8C=96=20-=20state?= =?UTF-8?q?=E6=A0=A1=E9=AA=8C/=E7=99=BB=E5=BD=95=E8=B7=B3=E8=BD=AC/useWech?= =?UTF-8?q?atCallback=20hook?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 回调页增加state防CSRF校验 - 登录成功后跳回登录前页面(login_redirect) - 新增useWechatCallback hook统一微信登录逻辑 - 绑定成功后更新authStore用户信息 - 移除重复的token存储逻辑(setAuth已处理) --- apps/web/src/hooks/useAuth.ts | 50 ++++++++++++++++++---- apps/web/src/pages/auth/Login.tsx | 7 +++ apps/web/src/pages/auth/WechatCallback.tsx | 31 ++++++++------ 3 files changed, 67 insertions(+), 21 deletions(-) diff --git a/apps/web/src/hooks/useAuth.ts b/apps/web/src/hooks/useAuth.ts index 1d3f0fec5..ae1a958cf 100644 --- a/apps/web/src/hooks/useAuth.ts +++ b/apps/web/src/hooks/useAuth.ts @@ -20,24 +20,56 @@ export const useLogin = () => { const data = await mutation.mutateAsync(credentials) const refreshToken = data.refresh_token ?? null - // 先保存 token - localStorage.setItem("access_token", data.access_token) - if (refreshToken) { - localStorage.setItem("refresh_token", refreshToken) - } else { - localStorage.removeItem("refresh_token") - } - // 获取用户信息 const user = await authApi.getCurrentUser() setAuth(user, data.access_token, refreshToken) - navigate("/") + + // 跳转到登录前页面或首页 + const redirect = localStorage.getItem("login_redirect") || "/" + localStorage.removeItem("login_redirect") + navigate(redirect, { replace: true }) return data } return { ...mutation, mutateAsync: login } } +// 微信登录 Hook(用于回调后处理登录状态) +export const useWechatCallback = () => { + const navigate = useNavigate() + const setAuth = useAuthStore((state) => state.setAuth) + const setUser = useAuthStore((state) => state.setUser) + + const mutation = useMutation({ + mutationFn: ({ code, state }: { code: string; state: string }) => + authApi.wechatCallback(code, state), + }) + + const handleCallback = async (code: string, state: string) => { + // 校验 state + const savedState = localStorage.getItem("wechat_state") + if (!savedState || savedState !== state) { + throw new Error("安全校验失败") + } + localStorage.removeItem("wechat_state") + + const result = await mutation.mutateAsync({ code, state }) + const user = await authApi.getCurrentUser() + setAuth(user, result.access_token, result.refresh_token) + + return { ...result, user } + } + + // 绑定成功后跳转 + const finishLogin = () => { + const redirect = localStorage.getItem("login_redirect") || "/" + localStorage.removeItem("login_redirect") + navigate(redirect, { replace: true }) + } + + return { ...mutation, handleCallback, finishLogin, setUser } +} + // 注册 Hook export const useRegister = () => { const navigate = useNavigate() diff --git a/apps/web/src/pages/auth/Login.tsx b/apps/web/src/pages/auth/Login.tsx index 6a4e22abb..1b92d9eca 100644 --- a/apps/web/src/pages/auth/Login.tsx +++ b/apps/web/src/pages/auth/Login.tsx @@ -41,6 +41,13 @@ const Login: React.FC = () => { const result = await getWechatAuthUrl() // 保存 state 到 localStorage 用于回调时验证 localStorage.setItem("wechat_state", result.state) + // 记录登录前的来源页,登录成功后跳回 + const from = window.location.pathname + window.location.search + if (from !== "/login" && from !== "/register") { + localStorage.setItem("login_redirect", from) + } else { + localStorage.removeItem("login_redirect") + } // 跳转到微信授权页 window.location.href = result.auth_url } catch (error) { diff --git a/apps/web/src/pages/auth/WechatCallback.tsx b/apps/web/src/pages/auth/WechatCallback.tsx index 41d46886e..783198149 100644 --- a/apps/web/src/pages/auth/WechatCallback.tsx +++ b/apps/web/src/pages/auth/WechatCallback.tsx @@ -28,15 +28,16 @@ const WechatCallback: React.FC = () => { const handleCallback = async () => { try { - const result = await wechatCallback(code, state) - - // 保存 token - localStorage.setItem("access_token", result.access_token) - if (result.refresh_token) { - localStorage.setItem("refresh_token", result.refresh_token) - } else { - localStorage.removeItem("refresh_token") + // 校验 state,防止 CSRF + const savedState = localStorage.getItem("wechat_state") + if (!savedState || savedState !== state) { + setError("安全校验失败,请重新登录") + setLoading(false) + return } + localStorage.removeItem("wechat_state") + + const result = await wechatCallback(code, state) // 获取用户信息 const userData = await getCurrentUser() @@ -44,9 +45,11 @@ const WechatCallback: React.FC = () => { setAuth(user, result.access_token, result.refresh_token) if (result.binding_complete) { - // 已绑定,直接跳转到首页 + // 已绑定,跳转到登录前页面或首页 message.success("登录成功") - navigate("/app/dashboard") + const redirect = localStorage.getItem("login_redirect") || "/" + localStorage.removeItem("login_redirect") + navigate(redirect, { replace: true }) } else { // 未绑定,显示绑定弹窗 setLoading(false) @@ -61,10 +64,14 @@ const WechatCallback: React.FC = () => { handleCallback() }, [searchParams, navigate, setAuth]) - const handleBindSuccess = (_user: User) => { + const handleBindSuccess = (user: User) => { + const setUser = useAuthStore.getState().setUser + setUser(user) setShowBindModal(false) message.success("绑定成功") - navigate("/app/dashboard") + const redirect = localStorage.getItem("login_redirect") || "/" + localStorage.removeItem("login_redirect") + navigate(redirect, { replace: true }) } const handleBindCancel = () => { -- 2.54.0