fix(audit): 修复「退出登录」无日志(PC + 移动端)
现象:审计日志里 logout 记录数恒为 0,退出动作完全不可见。
根因有两处,缺一不可:
1. 移动端(App)根本没有发起过上报
settings.vue 的 handleLogout 只做 removeStorageSync + reLaunch,
一个请求都没发。而 uni.reLaunch 会销毁页面上下文、直接掐断未完成的
uni.request —— 所以必须「先 await 上报、再清 token 与跳转」。
2. PC 端存在时序竞态
axios 的请求拦截器在微任务里执行、现读 localStorage 取 token;
而原实现同步清空 localStorage 并立刻 navigate,拦截器跑到时 token
已经没了 → 请求不带 Authorization → 后端只能记成「未认证」。
注:只改 AuthContext 不够,调用方 AdminLayout / ProfilePage 原来是
`logout(); navigate(...)`,不等就跳转照样会掐断请求 —— 三处都得改。
改动:
- 移动端 handleLogout 改 async,await post("/auth/logout") 后再清 token
- AuthContext.logout 改 async(类型同步为 () => Promise<void>),
先 await 上报再清 token;失败静默,绝不阻断退出
- AdminLayout / ProfilePage 两个调用点补 await
- 全部用 try-catch 兜住:断网/超时也照退不误(JWT 无状态,
服务端本就不需要它成功)
This commit is contained in:
@ -72,8 +72,10 @@ export default function AdminLayout() {
|
||||
return <Navigate to="/admin/login" replace />;
|
||||
}
|
||||
|
||||
function handleLogout() {
|
||||
logout();
|
||||
async function handleLogout() {
|
||||
// 必须 await:logout() 要先完成审计上报再清 token,
|
||||
// 提前 navigate 会把请求掐断,退出就留不下痕
|
||||
await logout();
|
||||
navigate("/admin/login", { replace: true });
|
||||
}
|
||||
|
||||
|
||||
@ -28,7 +28,8 @@ interface AuthState {
|
||||
|
||||
interface AuthContextValue extends AuthState {
|
||||
login: (username: string, password: string) => Promise<void>;
|
||||
logout: () => void;
|
||||
/** 登出。async 是因为必须先 await 审计上报、再清 token —— 顺序反了会丢日志 */
|
||||
logout: () => Promise<void>;
|
||||
isAuthenticated: boolean;
|
||||
}
|
||||
|
||||
@ -113,12 +114,19 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
setState({ user, token: accessToken, loading: false });
|
||||
}, []);
|
||||
|
||||
const logout = useCallback(() => {
|
||||
// 先上报登出(审计留痕),再做本地清理 —— 顺序不能反:
|
||||
// api 的请求拦截器是从 localStorage 取 token 的,清掉之后就发不出这个请求了。
|
||||
// 刻意不 await、失败也不阻断:JWT 无状态,服务端本就不需要它成功,
|
||||
// 而用户点了「退出」必须退得掉,不能因为一次上报失败就卡住。
|
||||
logoutApi().catch(() => {});
|
||||
const logout = useCallback(async () => {
|
||||
// ⚠️ 必须【先 await 上报、再清 token】。两边顺序反了或不等,退出就留不下痕:
|
||||
// 1) axios 的请求拦截器是在微任务里执行的,它去 localStorage 读 token 时,
|
||||
// 同步的 logoutInternal() 早已把 token 清掉 → 请求不带 Authorization
|
||||
// → 后端只能记成「未认证」,退出归因不到人;
|
||||
// 2) 调用方点完退出还会立刻 navigate 到登录页,进一步压缩执行窗口。
|
||||
// 所以这里(async) + 调用方(await) 两处都得改,只改一处等于没改。
|
||||
// 失败绝不影响退出:JWT 无状态,服务端本就不需要它成功。
|
||||
try {
|
||||
await logoutApi();
|
||||
} catch {
|
||||
/* 静默:断网/超时也照退不误 */
|
||||
}
|
||||
logoutInternal();
|
||||
}, []);
|
||||
|
||||
|
||||
@ -15,8 +15,10 @@ export default function ProfilePage() {
|
||||
const displayName = user?.display_name || user?.username || "未知用户";
|
||||
const avatarChar = displayName.charAt(0);
|
||||
|
||||
function handleLogout() {
|
||||
logout();
|
||||
async function handleLogout() {
|
||||
// 必须 await:logout() 要先完成审计上报再清 token,
|
||||
// 提前 navigate 会把请求掐断,退出就留不下痕
|
||||
await logout();
|
||||
navigate("/admin/login", { replace: true });
|
||||
}
|
||||
|
||||
|
||||
@ -60,9 +60,10 @@ export async function getMe(token: string): Promise<UserInfo> {
|
||||
* JWT 无状态,服务端不会(也无法)吊销令牌,本地清 token 就是登出。
|
||||
* 但没有这个请求,前端的「退出」动作在审计里完全不可见,所以必须上报一次。
|
||||
*
|
||||
* 刻意走 axios 实例(而非上面几个函数的裸 fetch):它的请求拦截器会从
|
||||
* localStorage 自动注入 Authorization,从而让后端记录到真实操作人。
|
||||
* ⚠️ 因此调用方必须在清除本地 token 【之前】调用本函数。
|
||||
* ⚠️ 调用方必须 **await 本函数之后**才清 localStorage 与跳转:
|
||||
* axios 的请求拦截器在微任务里执行、需要现读 localStorage 取 token。
|
||||
* 若不等就同步清空并 navigate,请求会不带 Authorization(或直接被掐断),
|
||||
* 后端只能记成「未认证」,退出归因不到人 —— 实测审计里 logout 记录为 0。
|
||||
*/
|
||||
export async function logout(): Promise<void> {
|
||||
await api.post("/auth/logout");
|
||||
|
||||
@ -38,6 +38,7 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from "vue";
|
||||
import { checkAppUpdate } from "../../utils/ota";
|
||||
import { post } from "../../utils/request";
|
||||
|
||||
// ============================================================
|
||||
// 缓存清理策略 —— 黑名单式:只删「明确登记过的业务缓存」
|
||||
@ -167,15 +168,31 @@ async function handleCheckUpdate() {
|
||||
await checkAppUpdate({ manual: true });
|
||||
}
|
||||
|
||||
function handleLogout() {
|
||||
async function handleLogout() {
|
||||
// 退出是不可逆的(要重新输账号密码),按车间使用场景加一道确认防误触
|
||||
uni.showModal({
|
||||
title: "退出登录",
|
||||
content: "退出后需要重新输入账号密码,确定退出吗?",
|
||||
confirmText: "退出",
|
||||
cancelText: "取消",
|
||||
success: (res) => {
|
||||
success: async (res) => {
|
||||
if (!res.confirm) return;
|
||||
|
||||
// 🔴 必须【先 await 上报、再清 token】——两者顺序反了或不等,退出就留不下痕:
|
||||
// 1) uni.reLaunch 会销毁页面上下文,直接掐断尚未发出的 uni.request;
|
||||
// 2) 而 request.js 是在发送时才从 storage 读 access_token,
|
||||
// 先清 storage 的话请求会不带 Authorization,后端只能记成「未认证」。
|
||||
// 这里刻意 try/catch 兜住:上报失败(断网/超时)也绝不能挡住用户退出。
|
||||
uni.showLoading({ title: "退出中...", mask: true });
|
||||
try {
|
||||
await post("/auth/logout");
|
||||
} catch (e) {
|
||||
// 静默:JWT 无状态,服务端本就不需要它成功
|
||||
console.warn("[logout] 上报失败(不影响退出)", e);
|
||||
} finally {
|
||||
uni.hideLoading();
|
||||
}
|
||||
|
||||
try {
|
||||
uni.removeStorageSync("token");
|
||||
uni.removeStorageSync("access_token");
|
||||
|
||||
Reference in New Issue
Block a user