feat: 添加 H5 端 openid 一致性检查,更新路由守卫逻辑以清除不一致的缓存
This commit is contained in:
@@ -159,6 +159,18 @@ server: {
|
|||||||
- CRMEB 客户端**任何异常都优雅降级**(返回空昵称/头像,不阻断登录),并 `logger.warning` 记录,便于排查。
|
- CRMEB 客户端**任何异常都优雅降级**(返回空昵称/头像,不阻断登录),并 `logger.warning` 记录,便于排查。
|
||||||
- `DEV_MODE=1` 才启用 `POST /api/auth/dev-login`,生产必须为 `0`。
|
- `DEV_MODE=1` 才启用 `POST /api/auth/dev-login`,生产必须为 `0`。
|
||||||
- **CRMEB 匹配口径**:openid 按 appid 隔离。若 CRMEB 与小程序非同一 appid,应改用 **unionid** 匹配(小程序传 unionid,CRMEB 内部接口按 unionid 查;openid 仍作本系统主键)。
|
- **CRMEB 匹配口径**:openid 按 appid 隔离。若 CRMEB 与小程序非同一 appid,应改用 **unionid** 匹配(小程序传 unionid,CRMEB 内部接口按 unionid 查;openid 仍作本系统主键)。
|
||||||
|
- **H5 端 openid 一致性检查**(分享场景必做):当 H5 被其他用户分享的链接打开时,web-view 的 `localStorage` 可能残留原用户的 token。H5 的路由守卫(如 `ProtectedRoute`)必须比对**当前 URL 中的 openid** 与 **本地缓存 token 中的 openid**,不一致时清除缓存重新走登录流程:
|
||||||
|
```tsx
|
||||||
|
const urlOpenid = new URLSearchParams(window.location.search).get('openid');
|
||||||
|
if (urlOpenid && token) {
|
||||||
|
const payload = decodeToken(token);
|
||||||
|
if (payload.openid !== urlOpenid) {
|
||||||
|
clearToken(); // 清除原用户缓存
|
||||||
|
return <Navigate to="/" replace />; // 重新登录
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
> 否则会出现:B 打开 A 分享的链接,URL 中已是 B 的 openid,但 H5 读取到 A 的缓存 token,仍然展示 A 的数据。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -776,3 +788,4 @@ async function handleCopyLink(url: string) {
|
|||||||
- [ ] WebView Bridge: H5 复制链接优先用 `document.execCommand('copy')`,bridge 兜底
|
- [ ] WebView Bridge: H5 复制链接优先用 `document.execCommand('copy')`,bridge 兜底
|
||||||
- [ ] WebView Bridge: 确认微信 JS-SDK 已加载(`jweixin-1.2.0.js`),否则 `wx.miniProgram` 不可用
|
- [ ] WebView Bridge: 确认微信 JS-SDK 已加载(`jweixin-1.2.0.js`),否则 `wx.miniProgram` 不可用
|
||||||
- [ ] WebView Bridge: 生成分享图片场景使用 `html-to-image` 等截图库
|
- [ ] WebView Bridge: 生成分享图片场景使用 `html-to-image` 等截图库
|
||||||
|
- [ ] H5 身份一致性: 路由守卫中比对 URL openid 与缓存 token openid,不一致时清除缓存重新登录
|
||||||
|
|||||||
@@ -19,18 +19,33 @@ import { ProfilePage } from "./pages/ProfilePage";
|
|||||||
import { VoiceRecordPage } from "./pages/VoiceRecordPage";
|
import { VoiceRecordPage } from "./pages/VoiceRecordPage";
|
||||||
import { NotificationPage } from "./pages/NotificationPage";
|
import { NotificationPage } from "./pages/NotificationPage";
|
||||||
import { MobileLayout } from "./components/MobileLayout";
|
import { MobileLayout } from "./components/MobileLayout";
|
||||||
import { getToken } from "./services/api";
|
import { getToken, clearToken, decodeToken } from "./services/api";
|
||||||
|
|
||||||
/** 路由守卫:未登录跳转登录页 */
|
/** 路由守卫:未登录跳转登录页 */
|
||||||
function ProtectedRoute() {
|
function ProtectedRoute() {
|
||||||
const token = getToken();
|
const token = getToken();
|
||||||
const urlToken = new URLSearchParams(window.location.search).get('token');
|
const urlToken = new URLSearchParams(window.location.search).get('token');
|
||||||
|
const urlOpenid = new URLSearchParams(window.location.search).get('openid');
|
||||||
|
|
||||||
// 来自小程序的 token → 让 LoginPage 处理
|
// 来自小程序的 token → 让 LoginPage 处理
|
||||||
if (urlToken) {
|
if (urlToken) {
|
||||||
return <Navigate to={`/?token=${encodeURIComponent(urlToken)}`} replace />;
|
return <Navigate to={`/?token=${encodeURIComponent(urlToken)}`} replace />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// URL 中有 openid 且与本地缓存的 token 中的 openid 不一致 → 清除缓存重新登录
|
||||||
|
if (urlOpenid && token) {
|
||||||
|
try {
|
||||||
|
const payload = decodeToken(token);
|
||||||
|
if (payload.openid !== urlOpenid) {
|
||||||
|
console.log('[auth] openid 不一致: URL=' + urlOpenid + ', cache=' + payload.openid + ',清除缓存');
|
||||||
|
clearToken();
|
||||||
|
return <Navigate to="/" replace />;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[auth] token 解码失败', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
return <Navigate to="/" replace />;
|
return <Navigate to="/" replace />;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user