Spaces:
Running
Running
神代綺凛
commited on
Commit
•
55151cc
1
Parent(s):
4ba7e27
feat: iframe / window mode
Browse files- README.md +29 -3
- dev/index.html +15 -0
- dev/src/App.vue +64 -0
- dev/src/main.ts +5 -0
- dev/src/utils/openWindow.ts +39 -0
- index.html +1 -1
- src/App.vue +8 -9
- src/utils/const.ts +14 -0
- src/utils/postMessage.ts +24 -0
- src/utils/qrSSE.ts +2 -0
- src/vite-env.d.ts +2 -0
- tsconfig.json +1 -1
- vite.config.ts +4 -1
README.md
CHANGED
@@ -1,7 +1,33 @@
|
|
1 |
## 哔哩哔哩 cookie 获取工具
|
2 |
|
3 |
-
扫码登录获取B站 cookie
|
4 |
|
5 |
-
|
6 |
|
7 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
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 |
-
|
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 |
});
|