浏览代码

feat: 前端允许外部内嵌 iframe

wuwenyi 1 周之前
父节点
当前提交
c51bd342e3
共有 4 个文件被更改,包括 64 次插入 和 3 次删除
  1. 4 0
      src/main.ts
  2. 25 0
      src/utils/auth.ts
  3. 29 0
      src/utils/embed.ts
  4. 6 3
      vite.config.ts

+ 4 - 0
src/main.ts

@@ -26,8 +26,12 @@ import {
 } from '@element-plus/icons-vue'
 import App from './App.vue'
 import router from './router'
+import { initEmbed } from './utils/embed'
 import './style.css'
 
+// 宿主免登:必须在路由初始导航(app.use(router) 触发的首个守卫)之前完成 token 落库
+initEmbed()
+
 const app = createApp(App)
 
 // 按需注册图标

+ 25 - 0
src/utils/auth.ts

@@ -4,11 +4,32 @@ const TokenKey = 'Admin-Token'
 
 const isSecure = window.location.protocol === 'https:'
 
+/**
+ * 是否运行在 iframe 内(被 SCRM 等宿主系统内嵌)
+ * 跨域访问 window.top 会抛异常,同样说明处于 iframe 中
+ */
+export function isEmbedded(): boolean {
+  try {
+    return window.self !== window.top
+  } catch {
+    return true
+  }
+}
+
 export function getToken(): string | undefined {
+  // iframe 内 token 走 sessionStorage:跨站 iframe 不会携带 SameSite=Strict 的 Cookie,
+  // 独立访问时行为保持原样(Cookie)
+  if (isEmbedded()) {
+    return sessionStorage.getItem(TokenKey) || undefined
+  }
   return Cookies.get(TokenKey)
 }
 
 export function setToken(token: string): void {
+  if (isEmbedded()) {
+    sessionStorage.setItem(TokenKey, token)
+    return
+  }
   Cookies.set(TokenKey, token, {
     secure: isSecure,
     sameSite: 'Strict',
@@ -17,5 +38,9 @@ export function setToken(token: string): void {
 }
 
 export function removeToken(): void {
+  if (isEmbedded()) {
+    sessionStorage.removeItem(TokenKey)
+    return
+  }
   Cookies.remove(TokenKey)
 }

+ 29 - 0
src/utils/embed.ts

@@ -0,0 +1,29 @@
+import { getToken, setToken } from '@/utils/auth'
+
+/**
+ * 宿主免登适配(yiwise 一智同款:token 随 URL 带进来,落地页即目标页)
+ *
+ * 宿主(SCRM「审批流配置」页)把服务账号 JWT 直接拼进 iframe 地址:
+ *   {qqflow}/flow/designer#embedToken=xxx
+ * 本模块在应用挂载前执行:
+ *  1. 从 URL 片段(hash)取 embedToken → 写入登录态(iframe 内为 sessionStorage,见 utils/auth.ts);
+ *  2. history.replaceState 清掉地址片段(token 不留在地址栏/浏览器历史);
+ *  3. 随后路由守卫直接放行到 /flow/designer,无登录页中转。
+ *
+ * 相对 yiwise 把 access_token 放 query 的做法,这里放 hash 片段:
+ * hash 不会随请求发给网关/静态服务器,不会进访问日志。
+ */
+const EMBED_TOKEN_KEY = 'embedToken'
+
+export function initEmbed(): void {
+  const hash = window.location.hash
+  if (!hash || hash.indexOf(EMBED_TOKEN_KEY) < 0) return
+  const token = new URLSearchParams(hash.replace(/^#/, '')).get(EMBED_TOKEN_KEY)
+  if (!token) return
+  if (getToken() !== token) {
+    setToken(token)
+  }
+  // 清理地址栏片段,保留 path/query
+  window.history.replaceState({}, '', window.location.pathname + window.location.search)
+  console.info('[embed] 已按 URL 参数完成免登,直达目标页')
+}

+ 6 - 3
vite.config.ts

@@ -2,6 +2,9 @@ import { defineConfig } from 'vite'
 import vue from '@vitejs/plugin-vue'
 import { resolve } from 'path'
 
+// 后端代理目标:默认 localhost:8080,可用 VITE_PROXY_TARGET 覆盖(如本地 8080 被其他服务占用)
+const backendTarget = process.env.VITE_PROXY_TARGET || 'http://localhost:8080'
+
 export default defineConfig(({ mode }) => ({
   base: '/',
   plugins: [vue()],
@@ -28,16 +31,16 @@ export default defineConfig(({ mode }) => ({
     port: 3000,
     proxy: {
       '/api': {
-        target: 'http://localhost:8080',
+        target: backendTarget,
         changeOrigin: true,
         rewrite: (path) => path.replace(/^\/api/, '')
       },
       '/uploads': {
-        target: 'http://localhost:8080',
+        target: backendTarget,
         changeOrigin: true
       },
       '/analysis': {
-        target: 'http://localhost:8080',
+        target: backendTarget,
         changeOrigin: true
       }
     }