feat: 公众端跳转登录时记录 redirect,登录后安全回跳

Made-with: Cursor
This commit is contained in:
zhonghua 2026-04-17 15:04:06 +08:00
parent 39f0d074a2
commit 65a8e0b127
2 changed files with 52 additions and 7 deletions

View File

@ -1,5 +1,26 @@
import axios from "axios";
/** 公众端登录页路径(与路由一致) */
const PUBLIC_LOGIN_PATH = "/p/login";
/**
* redirectpathname + search + hash
*/
function redirectToPublicLogin(): void {
const path = window.location.pathname + window.location.search + window.location.hash;
// 已在登录页:不再叠加 redirect避免循环
if (
path === PUBLIC_LOGIN_PATH ||
path.startsWith(PUBLIC_LOGIN_PATH + "?") ||
path.startsWith(PUBLIC_LOGIN_PATH + "#")
) {
window.location.href = PUBLIC_LOGIN_PATH;
return;
}
const redirect = encodeURIComponent(path);
window.location.href = `${PUBLIC_LOGIN_PATH}?redirect=${redirect}`;
}
// 公众端专用 axios 实例
const publicApi = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || "/api",
@ -14,9 +35,9 @@ publicApi.interceptors.request.use((config) => {
if (isTokenExpired(token)) {
localStorage.removeItem("public_token");
localStorage.removeItem("public_user");
// 如果在公众端页面,跳转到登录页
// 如果在公众端页面,跳转到登录页(记录当前路径)
if (window.location.pathname.startsWith("/p/")) {
window.location.href = "/p/login";
redirectToPublicLogin();
}
return config;
}
@ -70,9 +91,9 @@ publicApi.interceptors.response.use(
if (error.response?.status === 401) {
localStorage.removeItem("public_token");
localStorage.removeItem("public_user");
// 如果在公众端页面,跳转到公众端登录
// 如果在公众端页面,跳转到公众端登录(记录当前路径)
if (window.location.pathname.startsWith("/p/")) {
window.location.href = "/p/login";
redirectToPublicLogin();
}
}
return Promise.reject(error);

View File

@ -86,6 +86,30 @@ import type { Rule } from "ant-design-vue/es/form"
const router = useRouter()
const route = useRoute()
const DEFAULT_AFTER_LOGIN = "/p/activities"
/** 解析登录成功后的跳转地址:仅允许站内 `/p/` 路径,防止开放重定向 */
function resolveRedirectAfterLogin(raw: unknown): string {
if (typeof raw !== "string" || !raw.trim()) return DEFAULT_AFTER_LOGIN
try {
const decoded = decodeURIComponent(raw.trim())
if (!decoded.startsWith("/p/")) return DEFAULT_AFTER_LOGIN
if (decoded.startsWith("//") || /:\/\/|^\s*javascript:/i.test(decoded)) {
return DEFAULT_AFTER_LOGIN
}
//
if (
decoded.split("?")[0] === "/p/login" ||
decoded.split("?")[0].startsWith("/p/login/")
) {
return DEFAULT_AFTER_LOGIN
}
return decoded
} catch {
return DEFAULT_AFTER_LOGIN
}
}
const loading = ref(false)
const isRegister = ref(false)
const loginMethod = ref<'password' | 'sms'>('password')
@ -294,9 +318,9 @@ const handleSubmit = async () => {
return
}
//
const redirect = (route.query.redirect as string) || "/p/activities"
router.push(redirect)
// query.redirect public API
const target = resolveRedirectAfterLogin(route.query.redirect)
router.push(target)
} catch (error: any) {
const msg =
error?.response?.data?.message ||