神代綺凛 commited on
Commit
55151cc
1 Parent(s): 4ba7e27

feat: iframe / window mode

Browse files
README.md CHANGED
@@ -1,7 +1,33 @@
1
  ## 哔哩哔哩 cookie 获取工具
2
 
3
- 扫码登录获取B站 cookie,用于各种开发场景
4
 
5
- ### TODO
6
 
7
- - [ ] iframe / 窗口 模式
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ## 哔哩哔哩 cookie 获取工具
2
 
3
+ 扫码登录获取B站 cookie,可与各种场景联动
4
 
5
+ ## iframe / window 模式
6
 
7
+ 该工具可以通过 `<iframe>` `window.open()` 联动使用
8
+
9
+ 1. 编译时设置环境变量 `TRUST_ORIGIN`,值为你允许通过 `<iframe>` 或 `window.open()` 调用的源,多个之间用英文逗号 `,` 分隔,例如 `https://example.com,https://abc.example.com`
10
+ - 不设置时则无法正常使用 iframe / window 模式
11
+ - 可设置为 `*`,允许所有页面调用,但不推荐,除非你知道这意味着什么
12
+ - 在本地开发时若未提供则默认为 `*`
13
+ 2. 带上查询参数 `mode=iframe` 或 `mode=window` 进行使用(访问),然后监听 `message` 事件获取登陆成功后的 cookie
14
+ - 请参考 [demo](https://github.com/Tsuk1ko/bilibili-qr-login/blob/main/dev/src/App.vue)
15
+ - 本地开发访问 http://localhost:5173/dev/ 可体验效果
16
+
17
+ ## 开发
18
+
19
+ 要求 Node.js >= 18
20
+
21
+ ```bash
22
+ # 安装依赖
23
+ yarn
24
+
25
+ # 开发
26
+ yarn dev
27
+
28
+ # 编译
29
+ yarn build
30
+
31
+ # 预览成品(需要先编译)
32
+ yarn preview # http://localhost:3000
33
+ ```
dev/index.html ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta
6
+ name="viewport"
7
+ content="initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no, width=device-width"
8
+ />
9
+ <title>开发专用页面</title>
10
+ </head>
11
+ <body>
12
+ <div id="app"></div>
13
+ <script type="module" src="./src/main.ts"></script>
14
+ </body>
15
+ </html>
dev/src/App.vue ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <template>
2
+ <button class="test-btn" @click="openWindow">打开新窗口</button>
3
+ <button class="test-btn" @click="toggleIframe">{{ showIframe ? '关闭' : '打开' }} iframe</button>
4
+ <iframe v-if="showIframe" src="/?mode=iframe" width="380" height="340" style="border: none" />
5
+ <div class="cookie-box">
6
+ <CookieDisplay v-if="cookieResult" :value="cookieResult" />
7
+ </div>
8
+ </template>
9
+
10
+ <script setup lang="ts">
11
+ import { onBeforeUnmount, ref } from 'vue';
12
+ import CookieDisplay from '../../src/components/CookieDisplay.vue';
13
+ import { openQrWindow } from './utils/openWindow';
14
+
15
+ const cookieResult = ref('');
16
+ const showIframe = ref(false);
17
+
18
+ const openWindow = () => {
19
+ cookieResult.value = '';
20
+ openQrWindow('/?mode=window');
21
+ };
22
+
23
+ const toggleIframe = () => {
24
+ cookieResult.value = '';
25
+ showIframe.value = !showIframe.value;
26
+ };
27
+
28
+ interface QrMessage {
29
+ type: 'success';
30
+ mode: 'window' | 'iframe';
31
+ data: string;
32
+ }
33
+
34
+ const handleMessage = (e: MessageEvent<QrMessage>) => {
35
+ // 【重要】校验 message 来自扫码窗口/iframe
36
+ if (e.origin !== window.location.origin) return;
37
+
38
+ const { type, data, mode } = e.data;
39
+ if (type === 'success') {
40
+ cookieResult.value = data;
41
+ if (mode === 'window') {
42
+ (e.source as Window | null)?.close();
43
+ } else if (mode === 'iframe') {
44
+ showIframe.value = false;
45
+ }
46
+ }
47
+ };
48
+
49
+ window.addEventListener('message', handleMessage);
50
+
51
+ onBeforeUnmount(() => {
52
+ window.removeEventListener('message', handleMessage);
53
+ });
54
+ </script>
55
+
56
+ <style scoped lang="less">
57
+ .test-btn {
58
+ margin-bottom: 16px;
59
+ }
60
+
61
+ .cookie-box {
62
+ min-height: 180px;
63
+ }
64
+ </style>
dev/src/main.ts ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ import '../../src/style.less';
2
+ import { createApp } from 'vue';
3
+ import App from './App.vue';
4
+
5
+ createApp(App).mount('#app');
dev/src/utils/openWindow.ts ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ interface ScreenExt extends Screen {
2
+ availLeft: number;
3
+ availTop: number;
4
+ }
5
+
6
+ const getCenterPosition = (width: number, height: number) => {
7
+ const screenLeft = window.screenLeft !== undefined ? window.screenLeft : (window.screen as ScreenExt).availLeft;
8
+ const screenTop = window.screenTop !== undefined ? window.screenTop : (window.screen as ScreenExt).availTop;
9
+
10
+ const screenWidth = window.screen.width || window.outerWidth || document.documentElement.clientWidth;
11
+ const screenHeight = window.screen.height || window.outerWidth || document.documentElement.clientHeight;
12
+
13
+ return {
14
+ left: Math.round((screenWidth - width) / 2 + screenLeft),
15
+ top: Math.round((screenHeight - height) / 2 + screenTop),
16
+ };
17
+ };
18
+
19
+ const getFeaturesStr = (features: Record<string, any>) =>
20
+ Object.entries(features)
21
+ .map(([k, v]) => `${k}=${v}`)
22
+ .join(',');
23
+
24
+ export const openQrWindow = (url: string) => {
25
+ const width = 380;
26
+ const height = 340;
27
+ const features = getFeaturesStr({
28
+ width,
29
+ height,
30
+ location: false,
31
+ menubar: false,
32
+ resizeable: false,
33
+ scrollbars: false,
34
+ status: false,
35
+ toolbar: false,
36
+ ...getCenterPosition(width, height),
37
+ });
38
+ return window.open(url, '_blank', features);
39
+ };
index.html CHANGED
@@ -6,7 +6,7 @@
6
  name="viewport"
7
  content="initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no, width=device-width"
8
  />
9
- <title>哔哩哔哩 cookie 获取工具</title>
10
  </head>
11
  <body>
12
  <div id="app"></div>
 
6
  name="viewport"
7
  content="initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no, width=device-width"
8
  />
9
+ <title></title>
10
  </head>
11
  <body>
12
  <div id="app"></div>
src/App.vue CHANGED
@@ -1,8 +1,8 @@
1
  <template>
2
  <div class="text-center no-select">
3
- <h2>哔哩哔哩 cookie 获取工具</h2>
4
  <p>
5
- 使用手机 APP 扫码登录后即可获取 cookie (<a
6
  class="link"
7
  href="https://github.com/Tsuk1ko/bilibili-qr-login"
8
  target="_blank"
@@ -21,32 +21,31 @@
21
  <div class="text-center no-select">
22
  <p>{{ getters.statusText }}</p>
23
  </div>
24
- <div class="cookie-box">
25
  <CookieDisplay v-if="state.cookie" :value="state.cookie" />
26
  </div>
27
  </template>
28
 
29
  <script setup lang="ts">
30
  import QrCode from '@chenfengyuan/vue-qrcode';
 
31
  import RefreshBtn from './components/RefreshBtn.vue';
32
  import CookieDisplay from './components/CookieDisplay.vue';
33
  import LoadingIcon from './components/LoadingIcon.vue';
34
  import CheckIcon from './assets/icons/check_circle.svg';
35
  import { useQrSSE, QrStatus } from './utils/qrSSE';
 
36
  import type { QRCodeRenderersOptions } from 'qrcode';
37
 
 
 
38
  const qrCodeOption: QRCodeRenderersOptions = {
39
  margin: 0,
40
  };
41
 
42
  const { state, getters, restart, stop } = useQrSSE();
43
 
44
- // 开发热重载时关闭上次的 SSE
45
- if (import.meta.hot) {
46
- import.meta.hot.accept(() => {
47
- stop();
48
- });
49
- }
50
  </script>
51
 
52
  <style scoped lang="less">
 
1
  <template>
2
  <div class="text-center no-select">
3
+ <h2 v-if="!PARAM_MODE">哔哩哔哩 cookie 获取工具</h2>
4
  <p>
5
+ {{ PARAM_MODE ? '请使用哔哩哔哩手机 APP 扫码登录' : '使用手机 APP 扫码登录后即可获取 cookie' }} (<a
6
  class="link"
7
  href="https://github.com/Tsuk1ko/bilibili-qr-login"
8
  target="_blank"
 
21
  <div class="text-center no-select">
22
  <p>{{ getters.statusText }}</p>
23
  </div>
24
+ <div v-if="!PARAM_MODE" class="cookie-box">
25
  <CookieDisplay v-if="state.cookie" :value="state.cookie" />
26
  </div>
27
  </template>
28
 
29
  <script setup lang="ts">
30
  import QrCode from '@chenfengyuan/vue-qrcode';
31
+ import { onBeforeUnmount } from 'vue';
32
  import RefreshBtn from './components/RefreshBtn.vue';
33
  import CookieDisplay from './components/CookieDisplay.vue';
34
  import LoadingIcon from './components/LoadingIcon.vue';
35
  import CheckIcon from './assets/icons/check_circle.svg';
36
  import { useQrSSE, QrStatus } from './utils/qrSSE';
37
+ import { PARAM_MODE } from './utils/const';
38
  import type { QRCodeRenderersOptions } from 'qrcode';
39
 
40
+ window.document.title = PARAM_MODE ? '登录哔哩哔哩' : '哔哩哔哩 cookie 获取工具';
41
+
42
  const qrCodeOption: QRCodeRenderersOptions = {
43
  margin: 0,
44
  };
45
 
46
  const { state, getters, restart, stop } = useQrSSE();
47
 
48
+ onBeforeUnmount(stop);
 
 
 
 
 
49
  </script>
50
 
51
  <style scoped lang="less">
src/utils/const.ts ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export const IS_DEV = process.env.NODE_ENV === 'development';
2
+
3
+ export const PARAM_MODE = (new URL(window.location.href).searchParams.get('mode') || '') as 'window' | 'iframe' | '';
4
+
5
+ const trustOriginStr = __TRUST_ORIGIN__;
6
+
7
+ export const TRUST_ALL_ORIGIN = trustOriginStr.trim() === '*';
8
+
9
+ export const trustOrigins = new Set(
10
+ trustOriginStr
11
+ .split(',')
12
+ .map(s => s.trim())
13
+ .filter(Boolean),
14
+ );
src/utils/postMessage.ts ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { PARAM_MODE, TRUST_ALL_ORIGIN, trustOrigins } from './const';
2
+
3
+ interface QrMessage {
4
+ type: 'success';
5
+ mode: string;
6
+ data: string;
7
+ }
8
+
9
+ export const postQrMessage = (data: Omit<QrMessage, 'mode'>) => {
10
+ if (!PARAM_MODE) return;
11
+ const targetWindow: Window | null =
12
+ PARAM_MODE === 'window' ? window.opener : PARAM_MODE === 'iframe' ? window.top : null;
13
+ if (!targetWindow) {
14
+ console.error('No target window');
15
+ return;
16
+ }
17
+ const origin = targetWindow.location.origin;
18
+ if (!TRUST_ALL_ORIGIN && !trustOrigins.has(origin)) {
19
+ console.warn('Untrusted origin:', origin);
20
+ return;
21
+ }
22
+ const message: QrMessage = { ...data, mode: PARAM_MODE };
23
+ targetWindow.postMessage(message, origin);
24
+ };
src/utils/qrSSE.ts CHANGED
@@ -1,4 +1,5 @@
1
  import { computed, reactive } from 'vue';
 
2
 
3
  enum SSEEvent {
4
  GENERATE = 'generate',
@@ -123,6 +124,7 @@ class QrSSE {
123
  case PollQrResultCode.SUCCESS:
124
  this.state.status = QrStatus.SUCCESS;
125
  this.state.cookie = cookie!;
 
126
  break;
127
  default:
128
  this.handleError(msg);
 
1
  import { computed, reactive } from 'vue';
2
+ import { postQrMessage } from './postMessage';
3
 
4
  enum SSEEvent {
5
  GENERATE = 'generate',
 
124
  case PollQrResultCode.SUCCESS:
125
  this.state.status = QrStatus.SUCCESS;
126
  this.state.cookie = cookie!;
127
+ postQrMessage({ type: 'success', data: cookie! });
128
  break;
129
  default:
130
  this.handleError(msg);
src/vite-env.d.ts CHANGED
@@ -1,2 +1,4 @@
1
  /// <reference types="vite/client" />
2
  /// <reference types="vite-svg-loader" />
 
 
 
1
  /// <reference types="vite/client" />
2
  /// <reference types="vite-svg-loader" />
3
+
4
+ declare const __TRUST_ORIGIN__: string;
tsconfig.json CHANGED
@@ -20,6 +20,6 @@
20
  "noUnusedParameters": true,
21
  "noFallthroughCasesInSwitch": true
22
  },
23
- "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"],
24
  "references": [{ "path": "./tsconfig.node.json" }]
25
  }
 
20
  "noUnusedParameters": true,
21
  "noFallthroughCasesInSwitch": true
22
  },
23
+ "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "dev/**/*.ts", "dev/**/*.tsx", "dev/**/*.vue"],
24
  "references": [{ "path": "./tsconfig.node.json" }]
25
  }
vite.config.ts CHANGED
@@ -4,7 +4,7 @@ import svgLoader from 'vite-svg-loader';
4
  import { startApiServer } from './server/utils';
5
 
6
  // https://vitejs.dev/config/
7
- export default defineConfig(({ command }) => {
8
  if (command === 'serve') startApiServer();
9
  return {
10
  plugins: [vue(), svgLoader()],
@@ -16,5 +16,8 @@ export default defineConfig(({ command }) => {
16
  build: {
17
  outDir: 'dist/static',
18
  },
 
 
 
19
  };
20
  });
 
4
  import { startApiServer } from './server/utils';
5
 
6
  // https://vitejs.dev/config/
7
+ export default defineConfig(({ command, mode }) => {
8
  if (command === 'serve') startApiServer();
9
  return {
10
  plugins: [vue(), svgLoader()],
 
16
  build: {
17
  outDir: 'dist/static',
18
  },
19
+ define: {
20
+ __TRUST_ORIGIN__: JSON.stringify(process.env.TRUST_ORIGIN || (mode === 'development' ? '*' : '')),
21
+ },
22
  };
23
  });