zy-web-gate 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +170 -0
- package/dist/zy-web-gate.js +143 -0
- package/dist/zy-web-gate.js.map +1 -0
- package/dist/zy-web-gate.umd.cjs +50 -0
- package/dist/zy-web-gate.umd.cjs.map +1 -0
- package/package.json +42 -0
package/README.md
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# zy-web-gate
|
|
2
|
+
|
|
3
|
+
纯前端「共享固定密码」页面访问门。框架无关,任何前端项目(Vue / React / 纯 HTML)都能用。
|
|
4
|
+
|
|
5
|
+
输入正确的共享密码后才能看到页面内容;验证一次后,**同主域下所有子站自动放行**,无需重复输入。新增子站只要接入本包即可纳入同一套门。
|
|
6
|
+
|
|
7
|
+
## 它是什么 / 不是什么
|
|
8
|
+
|
|
9
|
+
这是一道**前端页面层的访问遮挡**,属于「防君子不防小人」:
|
|
10
|
+
|
|
11
|
+
- ✅ 没输对密码看不到页面、输对能进、同主域跨子域不用重复输。
|
|
12
|
+
- ✅ 真密码**只存在校验接口侧**,前端 bundle 里没有任何密码信息(连 hash 都没有)。
|
|
13
|
+
- ❌ 不防 DevTools、不防伪造 cookie、不防直接扒静态资源 / 调 API。
|
|
14
|
+
- ❌ 不是账号体系、不是 OAuth、不是邮箱/短信验证码,就是「一个共享固定密码」。
|
|
15
|
+
|
|
16
|
+
如果你需要真正的安全鉴权,请用后端鉴权 / 身份系统,本包不适用。
|
|
17
|
+
|
|
18
|
+
## 工作原理
|
|
19
|
+
|
|
20
|
+
1. 进入任一子站时,先查父域 cookie(`Domain=example.com`)。
|
|
21
|
+
2. cookie 有效 → 直接放行,不弹任何 UI。
|
|
22
|
+
3. cookie 无效 → 弹出密码输入页(原生 DOM,Shadow DOM 隔离样式)。
|
|
23
|
+
4. 用户输入密码 → `POST` 给校验接口 → 接口比对后只返回「对 / 错」。
|
|
24
|
+
5. 正确 → 写父域 cookie → 放行;之后所有同主域子站读到该 cookie 即免输。
|
|
25
|
+
|
|
26
|
+
**为什么用 cookie 而不是 localStorage**:`localStorage` 按 origin 隔离,子域之间读不到,无法实现「一次验证、全子域通行」。cookie 可通过 `Domain` 设置到父域被全部子域共享,这是纯前端实现跨子域登录态的唯一可靠机制。
|
|
27
|
+
|
|
28
|
+
## 安装
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install zy-web-gate
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## 使用
|
|
35
|
+
|
|
36
|
+
在**挂载真实应用之前**调用 `ensureGate()`。它只在「已通过门禁」时 resolve,所以未通过时真实应用绝不会挂载(无内容闪现)。
|
|
37
|
+
|
|
38
|
+
校验接口已**内置在包里**,子站接入零参数即可。
|
|
39
|
+
|
|
40
|
+
### Vue3 + Vite
|
|
41
|
+
|
|
42
|
+
```js
|
|
43
|
+
// main.js
|
|
44
|
+
import { createApp } from "vue";
|
|
45
|
+
import App from "./App.vue";
|
|
46
|
+
import { ensureGate } from "zy-web-gate";
|
|
47
|
+
|
|
48
|
+
await ensureGate();
|
|
49
|
+
|
|
50
|
+
createApp(App).mount("#app");
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
> 顶层 `await` 需要入口是 ESM module(Vite 默认满足)。若环境不支持顶层 await,用 `.then()`:
|
|
54
|
+
>
|
|
55
|
+
> ```js
|
|
56
|
+
> ensureGate().then(() => {
|
|
57
|
+
> createApp(App).mount("#app");
|
|
58
|
+
> });
|
|
59
|
+
> ```
|
|
60
|
+
|
|
61
|
+
### React
|
|
62
|
+
|
|
63
|
+
```js
|
|
64
|
+
import { ensureGate } from "zy-web-gate";
|
|
65
|
+
|
|
66
|
+
ensureGate().then(() => {
|
|
67
|
+
ReactDOM.createRoot(document.getElementById("root")).render(<App />);
|
|
68
|
+
});
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### 纯 HTML(通过 CDN,无需打包工具)
|
|
72
|
+
|
|
73
|
+
用 UMD 产物,全局变量为 `ZyWebGate`:
|
|
74
|
+
|
|
75
|
+
```html
|
|
76
|
+
<script src="https://unpkg.com/zy-web-gate"></script>
|
|
77
|
+
<script>
|
|
78
|
+
ZyWebGate.ensureGate().then(function () {
|
|
79
|
+
document.getElementById("app").style.display = "";
|
|
80
|
+
});
|
|
81
|
+
</script>
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
或用 ESM 方式:
|
|
85
|
+
|
|
86
|
+
```html
|
|
87
|
+
<script type="module">
|
|
88
|
+
import { ensureGate } from "https://unpkg.com/zy-web-gate?module";
|
|
89
|
+
await ensureGate();
|
|
90
|
+
document.getElementById("app").style.display = "";
|
|
91
|
+
</script>
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## 校验接口约定
|
|
95
|
+
|
|
96
|
+
校验接口地址**写死在包内**(`src/index.js` 的 `API_URL`),子站无需关心。改地址时改这个常量、发新版本,子站升级依赖即可。
|
|
97
|
+
|
|
98
|
+
本包不含密码,校验交给该接口完成:
|
|
99
|
+
|
|
100
|
+
| | |
|
|
101
|
+
|---|---|
|
|
102
|
+
| 方法 | `POST` |
|
|
103
|
+
| 请求体 | `{ "password": "用户输入的密码" }` |
|
|
104
|
+
| 通过 | HTTP 200,`{ "code": 200, "data": { "match": true } }` |
|
|
105
|
+
| 密码错 | 任何 `data.match !== true` 的响应(HTTP 401 也可) |
|
|
106
|
+
|
|
107
|
+
判定规则:**只有「HTTP ok 且 `data.match === true`」算通过**,其余一律当密码错。网络/接口异常会单独提示「网络异常」而非「密码错误」。
|
|
108
|
+
|
|
109
|
+
> 接口需放行 CORS(响应带 `Access-Control-Allow-Origin`,并正确处理 `OPTIONS` 预检),否则前端跨域调不通。本调用不带 cookie,`Allow-Origin: *` 即可。建议接口侧加限流防暴力猜密码。
|
|
110
|
+
|
|
111
|
+
## 配置项
|
|
112
|
+
|
|
113
|
+
`ensureGate()` 全部参数可选(校验接口已内置):
|
|
114
|
+
|
|
115
|
+
| 选项 | 默认 | 说明 |
|
|
116
|
+
|---|---|---|
|
|
117
|
+
| `cookieName` | `"zy_web_gate"` | 登录态 cookie 名 |
|
|
118
|
+
| `cookieValue` | `"1"` | 写入的 cookie 值 |
|
|
119
|
+
| `cookieDomain` | 自动推断 | 父域;不传则由当前 host 推断(如 `a.example.com` → `example.com`)。localhost / IP 自动不写 Domain |
|
|
120
|
+
| `maxAgeDays` | `7` | 登录态有效天数 |
|
|
121
|
+
| `sameSite` | `"Lax"` | cookie SameSite |
|
|
122
|
+
| `secure` | 自动 | 按当前协议自动判断(https 为 true,本地 http 自动关闭) |
|
|
123
|
+
| `title` | `"访问验证"` | 密码页标题 |
|
|
124
|
+
| `subtitle` | `"请输入访问密码后继续。"` | 副标题 |
|
|
125
|
+
| `placeholder` | `"访问密码"` | 输入框占位 |
|
|
126
|
+
| `buttonText` | `"进入"` | 按钮文案 |
|
|
127
|
+
| `timeoutMs` | `10000` | 接口超时(毫秒) |
|
|
128
|
+
|
|
129
|
+
## 登出
|
|
130
|
+
|
|
131
|
+
```js
|
|
132
|
+
import { logoutGate } from "zy-web-gate";
|
|
133
|
+
|
|
134
|
+
logoutGate(); // 清除父域 cookie,下次进入任一子站会重新要求输入密码
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
> 若 `ensureGate` 用了自定义 `cookieName` / `cookieDomain`,`logoutGate` 要传相同的值才能删掉对应 cookie。
|
|
138
|
+
|
|
139
|
+
## 跨子域 / 父域说明
|
|
140
|
+
|
|
141
|
+
- 父域自动推断为「去掉最左一段」:`a.example.com` → `example.com`。多级子域或想固定时,显式传 `cookieDomain`。
|
|
142
|
+
- 浏览器禁止把 cookie 设到 Public Suffix List 上的公共后缀(如 `com`、`eu.org` 等),所以父域只会落到你自己的可注册域那一层——这正是我们要的,也避免 cookie 泄漏给同后缀下别人的站点。
|
|
143
|
+
- 全站需 HTTPS(Cloudflare Pages 默认满足),`Secure` cookie 才能写入。
|
|
144
|
+
|
|
145
|
+
## 新增子站如何纳入
|
|
146
|
+
|
|
147
|
+
纯前端方案没有「零接入自动保护」——每个子站都要接入本包(装包 + 入口 `await ensureGate()` 几行)。但因登录态是全子域共享 cookie,新站只要接入了,已验证用户进去不会再被拦。
|
|
148
|
+
|
|
149
|
+
校验接口已内置在包内,新站接入只需装包 + 入口调一行 `await ensureGate()`,可做成共享模板 / 脚手架复制即用。
|
|
150
|
+
|
|
151
|
+
## 本地验证
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
cd examples
|
|
155
|
+
npx serve . # 或 python3 -m http.server
|
|
156
|
+
# 浏览器打开 demo.html
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
本地 `localhost` 下 cookie 不写 Domain、Secure 自动关闭,可验证「输对进、输错拦、刷新免输」;跨子域共享需部署到真实 `*.example.com` 才能验证。
|
|
160
|
+
|
|
161
|
+
## 构建与发布
|
|
162
|
+
|
|
163
|
+
源码在 `src/`,发布前用 Vite 库模式构建出 `dist/`(ESM + UMD):
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
npm run build # 产出 dist/zy-web-gate.js (ESM) 和 dist/zy-web-gate.umd.cjs (UMD)
|
|
167
|
+
npm publish # prepublishOnly 会自动先 build
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
`package.json` 的 `files` 只包含 `dist` 和 `README.md`,源码与示例不进 npm 包。
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
//#region src/cookie.js
|
|
2
|
+
function e(e) {
|
|
3
|
+
let t = e || window.location.hostname;
|
|
4
|
+
if (t === "localhost" || !t.includes(".") || /^\d{1,3}(\.\d{1,3}){3}$/.test(t)) return "";
|
|
5
|
+
let n = t.split(".");
|
|
6
|
+
return n.length <= 2 ? t : n.slice(1).join(".");
|
|
7
|
+
}
|
|
8
|
+
function t(e) {
|
|
9
|
+
let t = encodeURIComponent(e) + "=", n = document.cookie ? document.cookie.split("; ") : [];
|
|
10
|
+
for (let e of n) if (e.indexOf(t) === 0) return decodeURIComponent(e.slice(t.length));
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
function n(e, t, n) {
|
|
14
|
+
let { domain: r, maxAgeSeconds: i, sameSite: a = "Lax", path: o = "/" } = n, s = n.secure === void 0 ? window.location.protocol === "https:" : n.secure, c = encodeURIComponent(e) + "=" + encodeURIComponent(t) + "; path=" + o + "; max-age=" + Math.floor(i) + "; samesite=" + a;
|
|
15
|
+
r && (c += "; domain=" + r), s && (c += "; secure"), document.cookie = c;
|
|
16
|
+
}
|
|
17
|
+
function r(e, t) {
|
|
18
|
+
let { domain: n, path: r = "/" } = t, i = encodeURIComponent(e) + "=; path=" + r + "; max-age=0";
|
|
19
|
+
n && (i += "; domain=" + n), document.cookie = i;
|
|
20
|
+
}
|
|
21
|
+
//#endregion
|
|
22
|
+
//#region src/verify.js
|
|
23
|
+
async function i(e, t, n = {}) {
|
|
24
|
+
let { timeoutMs: r = 1e4 } = n, i = new AbortController(), a = setTimeout(() => i.abort(), r), o;
|
|
25
|
+
try {
|
|
26
|
+
o = await fetch(e, {
|
|
27
|
+
method: "POST",
|
|
28
|
+
headers: { "Content-Type": "application/json" },
|
|
29
|
+
body: JSON.stringify({ password: t }),
|
|
30
|
+
signal: i.signal
|
|
31
|
+
});
|
|
32
|
+
} catch {
|
|
33
|
+
return clearTimeout(a), {
|
|
34
|
+
ok: !1,
|
|
35
|
+
networkError: !0,
|
|
36
|
+
message: "网络异常,请稍后重试"
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
clearTimeout(a);
|
|
40
|
+
let s = null;
|
|
41
|
+
try {
|
|
42
|
+
s = await o.json();
|
|
43
|
+
} catch {
|
|
44
|
+
s = null;
|
|
45
|
+
}
|
|
46
|
+
return o.ok && s && s.data && s.data.match === !0 ? { ok: !0 } : {
|
|
47
|
+
ok: !1,
|
|
48
|
+
networkError: !1,
|
|
49
|
+
message: "密码错误"
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
//#endregion
|
|
53
|
+
//#region src/ui.js
|
|
54
|
+
var a = "\n:host { all: initial; }\n.mask {\n position: fixed; inset: 0; z-index: 2147483647;\n display: flex; align-items: center; justify-content: center;\n background: #0f1115;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"PingFang SC\", \"Microsoft YaHei\", sans-serif;\n}\n.card {\n width: 320px; max-width: calc(100vw - 48px);\n padding: 32px 28px;\n background: #1b1e25; border-radius: 14px;\n box-shadow: 0 12px 40px rgba(0,0,0,.45);\n box-sizing: border-box;\n}\n.title { margin: 0 0 6px; font-size: 18px; font-weight: 600; color: #f2f4f8; }\n.subtitle { margin: 0 0 22px; font-size: 13px; color: #8a92a6; line-height: 1.5; }\n.field { position: relative; }\n.input {\n width: 100%; box-sizing: border-box;\n padding: 11px 13px; font-size: 14px;\n color: #f2f4f8; background: #11141a;\n border: 1px solid #2c313c; border-radius: 9px; outline: none;\n transition: border-color .15s;\n}\n.input:focus { border-color: #4c8dff; }\n.btn {\n width: 100%; margin-top: 14px; padding: 11px 0;\n font-size: 14px; font-weight: 600; color: #fff; cursor: pointer;\n background: #4c8dff; border: none; border-radius: 9px;\n transition: background .15s, opacity .15s;\n}\n.btn:hover { background: #3a7df0; }\n.btn:disabled { opacity: .6; cursor: not-allowed; }\n.error {\n min-height: 18px; margin-top: 12px;\n font-size: 12.5px; color: #ff6b6b; line-height: 1.4;\n}\n";
|
|
55
|
+
function o(e) {
|
|
56
|
+
let t = document.createElement("div");
|
|
57
|
+
t.setAttribute("data-zy-web-gate", "");
|
|
58
|
+
let n = t.attachShadow({ mode: "open" }), r = document.createElement("style");
|
|
59
|
+
r.textContent = a, n.appendChild(r);
|
|
60
|
+
let i = document.createElement("div");
|
|
61
|
+
i.className = "mask", i.innerHTML = "\n <div class=\"card\">\n <h1 class=\"title\"></h1>\n <p class=\"subtitle\"></p>\n <div class=\"field\">\n <input class=\"input\" type=\"password\" autocomplete=\"current-password\" />\n </div>\n <button class=\"btn\" type=\"button\"></button>\n <div class=\"error\" aria-live=\"polite\"></div>\n </div>\n ", n.appendChild(i), n.querySelector(".title").textContent = e.title, n.querySelector(".subtitle").textContent = e.subtitle;
|
|
62
|
+
let o = n.querySelector(".input"), s = n.querySelector(".btn"), c = n.querySelector(".error");
|
|
63
|
+
o.placeholder = e.placeholder, s.textContent = e.buttonText;
|
|
64
|
+
let l = !1;
|
|
65
|
+
async function u() {
|
|
66
|
+
if (l) return;
|
|
67
|
+
let t = o.value;
|
|
68
|
+
if (!t) {
|
|
69
|
+
c.textContent = "请输入密码";
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
l = !0, s.disabled = !0, c.textContent = "";
|
|
73
|
+
let n = s.textContent;
|
|
74
|
+
s.textContent = "验证中…";
|
|
75
|
+
let r;
|
|
76
|
+
try {
|
|
77
|
+
r = await e.onSubmit(t);
|
|
78
|
+
} finally {
|
|
79
|
+
l = !1, s.disabled = !1, s.textContent = n;
|
|
80
|
+
}
|
|
81
|
+
r && r.ok ? d() : (c.textContent = r && r.message || "密码错误", o.select());
|
|
82
|
+
}
|
|
83
|
+
s.addEventListener("click", u), o.addEventListener("keydown", (e) => {
|
|
84
|
+
e.key === "Enter" && u();
|
|
85
|
+
});
|
|
86
|
+
function d() {
|
|
87
|
+
t.remove();
|
|
88
|
+
}
|
|
89
|
+
return document.body.appendChild(t), setTimeout(() => o.focus(), 0), { destroy: d };
|
|
90
|
+
}
|
|
91
|
+
//#endregion
|
|
92
|
+
//#region src/index.js
|
|
93
|
+
var s = "https://m1.apifoxmock.com/m1/6205743-5899102-default/zy-check-password", c = {
|
|
94
|
+
cookieName: "zy_web_gate",
|
|
95
|
+
cookieValue: "1",
|
|
96
|
+
cookieDomain: void 0,
|
|
97
|
+
maxAgeDays: 7,
|
|
98
|
+
sameSite: "Lax",
|
|
99
|
+
secure: void 0,
|
|
100
|
+
title: "访问验证",
|
|
101
|
+
subtitle: "请输入访问密码后继续。",
|
|
102
|
+
placeholder: "访问密码",
|
|
103
|
+
buttonText: "进入",
|
|
104
|
+
timeoutMs: 1e4
|
|
105
|
+
};
|
|
106
|
+
function l(t) {
|
|
107
|
+
let n = {
|
|
108
|
+
...c,
|
|
109
|
+
...t || {}
|
|
110
|
+
};
|
|
111
|
+
return n.cookieDomain === void 0 && (n.cookieDomain = e()), n;
|
|
112
|
+
}
|
|
113
|
+
function u(e) {
|
|
114
|
+
let r = l(e);
|
|
115
|
+
return t(r.cookieName) === r.cookieValue ? Promise.resolve() : new Promise((e) => {
|
|
116
|
+
o({
|
|
117
|
+
title: r.title,
|
|
118
|
+
subtitle: r.subtitle,
|
|
119
|
+
placeholder: r.placeholder,
|
|
120
|
+
buttonText: r.buttonText,
|
|
121
|
+
onSubmit: async (t) => {
|
|
122
|
+
let a = await i(s, t, { timeoutMs: r.timeoutMs });
|
|
123
|
+
return a.ok && (n(r.cookieName, r.cookieValue, {
|
|
124
|
+
domain: r.cookieDomain,
|
|
125
|
+
maxAgeSeconds: r.maxAgeDays * 24 * 60 * 60,
|
|
126
|
+
sameSite: r.sameSite,
|
|
127
|
+
secure: r.secure
|
|
128
|
+
}), e()), a;
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
function d(t = {}) {
|
|
134
|
+
let n = {
|
|
135
|
+
...c,
|
|
136
|
+
...t
|
|
137
|
+
}, i = n.cookieDomain === void 0 ? e() : n.cookieDomain;
|
|
138
|
+
r(n.cookieName, { domain: i });
|
|
139
|
+
}
|
|
140
|
+
//#endregion
|
|
141
|
+
export { u as ensureGate, e as inferParentDomain, d as logoutGate, t as readCookie };
|
|
142
|
+
|
|
143
|
+
//# sourceMappingURL=zy-web-gate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"zy-web-gate.js","names":[],"sources":["../src/cookie.js","../src/verify.js","../src/ui.js","../src/index.js"],"sourcesContent":["// 跨子域共享登录态的核心:父域 cookie 读写。\n//\n// 为什么必须用 cookie 而不是 localStorage:\n// localStorage 按 origin(scheme+host+port)隔离,a.example.com 写的东西\n// b.example.com 读不到,无法实现「一次验证、全子域通行」。\n// cookie 可以通过 Domain 属性设置到父域,被该域下所有子域共享读取。\n\n/**\n * 由当前 host 推断可用于跨子域共享的父域。\n *\n * 例如当前在 a.example.com,应返回 example.com,\n * 这样写入的 cookie 能被 my./b./x. 等所有兄弟子域读到。\n *\n * 注意:eu.org 在 Public Suffix List 上,浏览器禁止把 cookie 设到 eu.org,\n * 所以父域只能取到 example.com 这一层,刚好是我们要的、也是安全的。\n *\n * 这里采用「去掉最左一段」的朴素策略:a.example.com -> example.com。\n * 大多数「单层子域」场景够用;若有多级子域或想显式指定,用 options.cookieDomain 覆盖。\n *\n * @param {string} [hostname] 默认取 location.hostname\n * @returns {string} 用于 cookie Domain 的父域;localhost / IP 等场景返回空串(表示按当前 host)\n */\nexport function inferParentDomain(hostname) {\n const host = hostname || window.location.hostname;\n\n // localhost、单段主机名、IP 地址:不设 Domain,cookie 只对当前 host 生效。\n if (host === \"localhost\" || !host.includes(\".\")) return \"\";\n if (/^\\d{1,3}(\\.\\d{1,3}){3}$/.test(host)) return \"\";\n\n const parts = host.split(\".\");\n // 形如 example.com(两段):父域就是它自己。\n if (parts.length <= 2) return host;\n // 形如 a.example.com:去掉最左一段。\n return parts.slice(1).join(\".\");\n}\n\n/**\n * 读取指定名称的 cookie 值。\n * @param {string} name\n * @returns {string|null}\n */\nexport function readCookie(name) {\n const prefix = encodeURIComponent(name) + \"=\";\n const items = document.cookie ? document.cookie.split(\"; \") : [];\n for (const item of items) {\n if (item.indexOf(prefix) === 0) {\n return decodeURIComponent(item.slice(prefix.length));\n }\n }\n return null;\n}\n\n/**\n * 写入跨子域 cookie。\n *\n * @param {string} name\n * @param {string} value\n * @param {object} opts\n * @param {string} opts.domain cookie 的 Domain,传空串则不写 Domain(仅当前 host)\n * @param {number} opts.maxAgeSeconds 有效期(秒)\n * @param {string} [opts.sameSite=\"Lax\"]\n * @param {boolean} [opts.secure=true] HTTPS 下应为 true;本地 http 调试时会自动放宽\n * @param {string} [opts.path=\"/\"]\n */\nexport function writeCookie(name, value, opts) {\n const {\n domain,\n maxAgeSeconds,\n sameSite = \"Lax\",\n path = \"/\",\n } = opts;\n\n // 本地 http 调试时 Secure 会让 cookie 写不进去,这里按当前协议自动判断。\n const secure = opts.secure !== undefined\n ? opts.secure\n : window.location.protocol === \"https:\";\n\n let str =\n encodeURIComponent(name) + \"=\" + encodeURIComponent(value) +\n \"; path=\" + path +\n \"; max-age=\" + Math.floor(maxAgeSeconds) +\n \"; samesite=\" + sameSite;\n\n if (domain) str += \"; domain=\" + domain;\n if (secure) str += \"; secure\";\n\n document.cookie = str;\n}\n\n/**\n * 删除跨子域 cookie(用于「登出」)。Domain / Path 必须和写入时一致才能删掉。\n * @param {string} name\n * @param {object} opts\n * @param {string} opts.domain\n * @param {string} [opts.path=\"/\"]\n */\nexport function deleteCookie(name, opts) {\n const { domain, path = \"/\" } = opts;\n let str = encodeURIComponent(name) + \"=; path=\" + path + \"; max-age=0\";\n if (domain) str += \"; domain=\" + domain;\n document.cookie = str;\n}\n","// 把用户输入的密码发给校验接口,由接口判断对错。\n//\n// 设计要点(方案③):真密码只存在接口侧,前端 bundle 里没有任何密码信息。\n// 前端只发 { password },只收「对 / 错」。改密码只改接口,子站零改动。\n\n/**\n * 校验结果。\n * @typedef {object} VerifyResult\n * @property {boolean} ok 密码是否正确\n * @property {boolean} [networkError] 是否因网络/接口异常导致(区别于「密码错」)\n * @property {string} [message] 可展示给用户的提示\n */\n\n/**\n * 调用校验接口。\n *\n * 接口约定:\n * 请求 POST apiUrl body: {\"password\": \"用户输入\"}\n * 通过 HTTP 200, {\"code\":200, \"data\":{\"match\":true}}\n * 密码错 HTTP 401, {\"code\":401, \"data\":{}}(或任何 match !== true 的响应)\n *\n * 判定规则:只有「HTTP ok 且 data.match === true」才算密码正确;\n * 其余一律当密码错。网络层异常单独标记为 networkError,便于 UI 区分提示。\n *\n * @param {string} apiUrl\n * @param {string} password\n * @param {object} [opts]\n * @param {number} [opts.timeoutMs=10000]\n * @returns {Promise<VerifyResult>}\n */\nexport async function verifyPassword(apiUrl, password, opts = {}) {\n const { timeoutMs = 10000 } = opts;\n\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n\n let resp;\n try {\n resp = await fetch(apiUrl, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ password }),\n signal: controller.signal,\n });\n } catch (err) {\n clearTimeout(timer);\n // fetch 抛错只可能是网络层问题(断网、CORS、超时 abort 等),不是密码错。\n return {\n ok: false,\n networkError: true,\n message: \"网络异常,请稍后重试\",\n };\n }\n clearTimeout(timer);\n\n let data = null;\n try {\n data = await resp.json();\n } catch {\n // 响应不是合法 JSON:当作密码错/异常处理,不放行。\n data = null;\n }\n\n const match = !!(resp.ok && data && data.data && data.data.match === true);\n\n if (match) return { ok: true };\n\n return {\n ok: false,\n networkError: false,\n message: \"密码错误\",\n };\n}\n","// 框架无关的密码输入页(原生 DOM)。\n//\n// 用 Shadow DOM 把样式和结构完全隔离,既不会污染子站的 CSS,\n// 也不会被子站的全局样式影响。任何前端项目(Vue / React / 纯 HTML)都能用。\n\nconst STYLE = `\n:host { all: initial; }\n.mask {\n position: fixed; inset: 0; z-index: 2147483647;\n display: flex; align-items: center; justify-content: center;\n background: #0f1115;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"PingFang SC\", \"Microsoft YaHei\", sans-serif;\n}\n.card {\n width: 320px; max-width: calc(100vw - 48px);\n padding: 32px 28px;\n background: #1b1e25; border-radius: 14px;\n box-shadow: 0 12px 40px rgba(0,0,0,.45);\n box-sizing: border-box;\n}\n.title { margin: 0 0 6px; font-size: 18px; font-weight: 600; color: #f2f4f8; }\n.subtitle { margin: 0 0 22px; font-size: 13px; color: #8a92a6; line-height: 1.5; }\n.field { position: relative; }\n.input {\n width: 100%; box-sizing: border-box;\n padding: 11px 13px; font-size: 14px;\n color: #f2f4f8; background: #11141a;\n border: 1px solid #2c313c; border-radius: 9px; outline: none;\n transition: border-color .15s;\n}\n.input:focus { border-color: #4c8dff; }\n.btn {\n width: 100%; margin-top: 14px; padding: 11px 0;\n font-size: 14px; font-weight: 600; color: #fff; cursor: pointer;\n background: #4c8dff; border: none; border-radius: 9px;\n transition: background .15s, opacity .15s;\n}\n.btn:hover { background: #3a7df0; }\n.btn:disabled { opacity: .6; cursor: not-allowed; }\n.error {\n min-height: 18px; margin-top: 12px;\n font-size: 12.5px; color: #ff6b6b; line-height: 1.4;\n}\n`;\n\n/**\n * 创建并挂载密码页。返回一个对象,可用于销毁。\n *\n * @param {object} cfg\n * @param {string} cfg.title\n * @param {string} cfg.subtitle\n * @param {string} cfg.placeholder\n * @param {string} cfg.buttonText\n * @param {(password: string) => Promise<{ok: boolean, message?: string}>} cfg.onSubmit\n * 提交回调;返回 ok=true 时 UI 自动销毁,false 时显示 message。\n * @returns {{ destroy: () => void }}\n */\nexport function mountPasswordGate(cfg) {\n const host = document.createElement(\"div\");\n host.setAttribute(\"data-zy-web-gate\", \"\");\n const shadow = host.attachShadow({ mode: \"open\" });\n\n const style = document.createElement(\"style\");\n style.textContent = STYLE;\n shadow.appendChild(style);\n\n const mask = document.createElement(\"div\");\n mask.className = \"mask\";\n mask.innerHTML = `\n <div class=\"card\">\n <h1 class=\"title\"></h1>\n <p class=\"subtitle\"></p>\n <div class=\"field\">\n <input class=\"input\" type=\"password\" autocomplete=\"current-password\" />\n </div>\n <button class=\"btn\" type=\"button\"></button>\n <div class=\"error\" aria-live=\"polite\"></div>\n </div>\n `;\n shadow.appendChild(mask);\n\n // 用 textContent 赋值,避免把配置文本当 HTML 注入。\n shadow.querySelector(\".title\").textContent = cfg.title;\n shadow.querySelector(\".subtitle\").textContent = cfg.subtitle;\n const input = shadow.querySelector(\".input\");\n const btn = shadow.querySelector(\".btn\");\n const errorEl = shadow.querySelector(\".error\");\n input.placeholder = cfg.placeholder;\n btn.textContent = cfg.buttonText;\n\n let submitting = false;\n\n async function submit() {\n if (submitting) return;\n const password = input.value;\n if (!password) {\n errorEl.textContent = \"请输入密码\";\n return;\n }\n\n submitting = true;\n btn.disabled = true;\n errorEl.textContent = \"\";\n const originalBtnText = btn.textContent;\n btn.textContent = \"验证中…\";\n\n let result;\n try {\n result = await cfg.onSubmit(password);\n } finally {\n submitting = false;\n btn.disabled = false;\n btn.textContent = originalBtnText;\n }\n\n if (result && result.ok) {\n destroy();\n } else {\n errorEl.textContent = (result && result.message) || \"密码错误\";\n input.select();\n }\n }\n\n btn.addEventListener(\"click\", submit);\n input.addEventListener(\"keydown\", (e) => {\n if (e.key === \"Enter\") submit();\n });\n\n function destroy() {\n host.remove();\n }\n\n document.body.appendChild(host);\n // 自动聚焦输入框,方便直接打字。\n setTimeout(() => input.focus(), 0);\n\n return { destroy };\n}\n","// zy-web-gate —— 纯前端「共享固定密码」页面访问门。\n//\n// 用法(在 Vue/React/任意前端入口、挂载真实应用之前调用):\n//\n// import { ensureGate } from \"zy-web-gate\";\n//\n// await ensureGate(); // 校验接口已固定在包内,零参数即可\n//\n// // 走到这里说明已通过门禁,再启动真实应用:\n// createApp(App).mount(\"#app\");\n//\n// 跨子域:验证通过后写父域 cookie(Domain=example.com),同主域其他子站\n// 读到该 cookie 即直接放行,无需再次输入密码。\n// 安全边界:纯前端页面遮挡,防君子不防小人;真密码只在校验接口侧。\n\nimport { inferParentDomain, readCookie, writeCookie, deleteCookie } from \"./cookie.js\";\nimport { verifyPassword } from \"./verify.js\";\nimport { mountPasswordGate } from \"./ui.js\";\n\n// 校验接口固定在包内(写死、不可传)。\n// 它不是秘密——本来就出现在前端网络请求里;写死可让子站接入零参数。\n// 改地址时只改这里、发新版本,子站升级依赖即可。\nconst API_URL = \"https://m1.apifoxmock.com/m1/6205743-5899102-default/zy-check-password\";\n\n/** @type {Required<GateOptions>} 默认配置 */\nconst DEFAULTS = {\n cookieName: \"zy_web_gate\",\n cookieValue: \"1\",\n cookieDomain: undefined, // undefined 表示自动推断父域\n maxAgeDays: 7,\n sameSite: \"Lax\",\n secure: undefined, // undefined 表示按当前协议自动判断\n title: \"访问验证\",\n subtitle: \"请输入访问密码后继续。\",\n placeholder: \"访问密码\",\n buttonText: \"进入\",\n timeoutMs: 10000,\n};\n\n/**\n * @typedef {object} GateOptions\n * @property {string} [cookieName] 登录态 cookie 名,默认 \"zy_web_gate\"\n * @property {string} [cookieValue] 写入的 cookie 值,默认 \"1\"\n * @property {string} [cookieDomain] 显式指定父域;不传则自动推断(如 example.com)\n * @property {number} [maxAgeDays] 登录态有效天数,默认 7\n * @property {string} [sameSite] cookie SameSite,默认 \"Lax\"\n * @property {boolean} [secure] cookie Secure;不传则按当前协议自动判断\n * @property {string} [title] 密码页标题\n * @property {string} [subtitle] 密码页副标题\n * @property {string} [placeholder] 输入框占位文案\n * @property {string} [buttonText] 按钮文案\n * @property {number} [timeoutMs] 接口超时毫秒,默认 10000\n */\n\n/**\n * 解析最终配置。\n * @param {GateOptions} [options]\n */\nfunction resolveConfig(options) {\n const cfg = { ...DEFAULTS, ...(options || {}) };\n if (cfg.cookieDomain === undefined) {\n cfg.cookieDomain = inferParentDomain();\n }\n return cfg;\n}\n\n/**\n * 访问门主入口。\n *\n * 行为:\n * 1. 若父域 cookie 已存在有效登录态 -> 直接 resolve(放行,无 UI)。\n * 2. 否则弹出密码页,等用户输入并通过接口校验。\n * 3. 校验通过 -> 写父域 cookie -> resolve。\n *\n * 该 Promise 只在「已通过门禁」时 resolve;密码未通过时不会 resolve,\n * 因此把它放在 createApp().mount() 之前 await,可保证未通过时真实应用绝不挂载。\n *\n * @param {GateOptions} [options]\n * @returns {Promise<void>}\n */\nexport function ensureGate(options) {\n const cfg = resolveConfig(options);\n\n // 已有登录态:直接放行。\n if (readCookie(cfg.cookieName) === cfg.cookieValue) {\n return Promise.resolve();\n }\n\n return new Promise((resolve) => {\n mountPasswordGate({\n title: cfg.title,\n subtitle: cfg.subtitle,\n placeholder: cfg.placeholder,\n buttonText: cfg.buttonText,\n onSubmit: async (password) => {\n const result = await verifyPassword(API_URL, password, {\n timeoutMs: cfg.timeoutMs,\n });\n if (result.ok) {\n writeCookie(cfg.cookieName, cfg.cookieValue, {\n domain: cfg.cookieDomain,\n maxAgeSeconds: cfg.maxAgeDays * 24 * 60 * 60,\n sameSite: cfg.sameSite,\n secure: cfg.secure,\n });\n // 通过门禁:UI 会在本回调返回 ok 后自动销毁,这里同时 resolve 外层 Promise,\n // 让 await ensureGate() 的调用方继续往下挂载真实应用。\n resolve();\n }\n return result;\n },\n });\n });\n}\n\n/**\n * 主动登出:清除父域 cookie。调用后下次进入任一子站会重新要求输入密码。\n * @param {GateOptions} options 至少需与 ensureGate 一致的 cookieName / cookieDomain\n */\nexport function logoutGate(options = {}) {\n const cfg = { ...DEFAULTS, ...options };\n const domain = cfg.cookieDomain === undefined\n ? inferParentDomain()\n : cfg.cookieDomain;\n deleteCookie(cfg.cookieName, { domain });\n}\n\nexport { inferParentDomain, readCookie } from \"./cookie.js\";\n"],"mappings":"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"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
(function(e,t){typeof exports==`object`&&typeof module<`u`?t(exports):typeof define==`function`&&define.amd?define([`exports`],t):(e=typeof globalThis<`u`?globalThis:e||self,t(e.ZyWebGate={}))})(this,function(e){Object.defineProperty(e,Symbol.toStringTag,{value:`Module`});function t(e){let t=e||window.location.hostname;if(t===`localhost`||!t.includes(`.`)||/^\d{1,3}(\.\d{1,3}){3}$/.test(t))return``;let n=t.split(`.`);return n.length<=2?t:n.slice(1).join(`.`)}function n(e){let t=encodeURIComponent(e)+`=`,n=document.cookie?document.cookie.split(`; `):[];for(let e of n)if(e.indexOf(t)===0)return decodeURIComponent(e.slice(t.length));return null}function r(e,t,n){let{domain:r,maxAgeSeconds:i,sameSite:a=`Lax`,path:o=`/`}=n,s=n.secure===void 0?window.location.protocol===`https:`:n.secure,c=encodeURIComponent(e)+`=`+encodeURIComponent(t)+`; path=`+o+`; max-age=`+Math.floor(i)+`; samesite=`+a;r&&(c+=`; domain=`+r),s&&(c+=`; secure`),document.cookie=c}function i(e,t){let{domain:n,path:r=`/`}=t,i=encodeURIComponent(e)+`=; path=`+r+`; max-age=0`;n&&(i+=`; domain=`+n),document.cookie=i}async function a(e,t,n={}){let{timeoutMs:r=1e4}=n,i=new AbortController,a=setTimeout(()=>i.abort(),r),o;try{o=await fetch(e,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({password:t}),signal:i.signal})}catch{return clearTimeout(a),{ok:!1,networkError:!0,message:`网络异常,请稍后重试`}}clearTimeout(a);let s=null;try{s=await o.json()}catch{s=null}return o.ok&&s&&s.data&&s.data.match===!0?{ok:!0}:{ok:!1,networkError:!1,message:`密码错误`}}var o=`
|
|
2
|
+
:host { all: initial; }
|
|
3
|
+
.mask {
|
|
4
|
+
position: fixed; inset: 0; z-index: 2147483647;
|
|
5
|
+
display: flex; align-items: center; justify-content: center;
|
|
6
|
+
background: #0f1115;
|
|
7
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", "Microsoft YaHei", sans-serif;
|
|
8
|
+
}
|
|
9
|
+
.card {
|
|
10
|
+
width: 320px; max-width: calc(100vw - 48px);
|
|
11
|
+
padding: 32px 28px;
|
|
12
|
+
background: #1b1e25; border-radius: 14px;
|
|
13
|
+
box-shadow: 0 12px 40px rgba(0,0,0,.45);
|
|
14
|
+
box-sizing: border-box;
|
|
15
|
+
}
|
|
16
|
+
.title { margin: 0 0 6px; font-size: 18px; font-weight: 600; color: #f2f4f8; }
|
|
17
|
+
.subtitle { margin: 0 0 22px; font-size: 13px; color: #8a92a6; line-height: 1.5; }
|
|
18
|
+
.field { position: relative; }
|
|
19
|
+
.input {
|
|
20
|
+
width: 100%; box-sizing: border-box;
|
|
21
|
+
padding: 11px 13px; font-size: 14px;
|
|
22
|
+
color: #f2f4f8; background: #11141a;
|
|
23
|
+
border: 1px solid #2c313c; border-radius: 9px; outline: none;
|
|
24
|
+
transition: border-color .15s;
|
|
25
|
+
}
|
|
26
|
+
.input:focus { border-color: #4c8dff; }
|
|
27
|
+
.btn {
|
|
28
|
+
width: 100%; margin-top: 14px; padding: 11px 0;
|
|
29
|
+
font-size: 14px; font-weight: 600; color: #fff; cursor: pointer;
|
|
30
|
+
background: #4c8dff; border: none; border-radius: 9px;
|
|
31
|
+
transition: background .15s, opacity .15s;
|
|
32
|
+
}
|
|
33
|
+
.btn:hover { background: #3a7df0; }
|
|
34
|
+
.btn:disabled { opacity: .6; cursor: not-allowed; }
|
|
35
|
+
.error {
|
|
36
|
+
min-height: 18px; margin-top: 12px;
|
|
37
|
+
font-size: 12.5px; color: #ff6b6b; line-height: 1.4;
|
|
38
|
+
}
|
|
39
|
+
`;function s(e){let t=document.createElement(`div`);t.setAttribute(`data-zy-web-gate`,``);let n=t.attachShadow({mode:`open`}),r=document.createElement(`style`);r.textContent=o,n.appendChild(r);let i=document.createElement(`div`);i.className=`mask`,i.innerHTML=`
|
|
40
|
+
<div class="card">
|
|
41
|
+
<h1 class="title"></h1>
|
|
42
|
+
<p class="subtitle"></p>
|
|
43
|
+
<div class="field">
|
|
44
|
+
<input class="input" type="password" autocomplete="current-password" />
|
|
45
|
+
</div>
|
|
46
|
+
<button class="btn" type="button"></button>
|
|
47
|
+
<div class="error" aria-live="polite"></div>
|
|
48
|
+
</div>
|
|
49
|
+
`,n.appendChild(i),n.querySelector(`.title`).textContent=e.title,n.querySelector(`.subtitle`).textContent=e.subtitle;let a=n.querySelector(`.input`),s=n.querySelector(`.btn`),c=n.querySelector(`.error`);a.placeholder=e.placeholder,s.textContent=e.buttonText;let l=!1;async function u(){if(l)return;let t=a.value;if(!t){c.textContent=`请输入密码`;return}l=!0,s.disabled=!0,c.textContent=``;let n=s.textContent;s.textContent=`验证中…`;let r;try{r=await e.onSubmit(t)}finally{l=!1,s.disabled=!1,s.textContent=n}r&&r.ok?d():(c.textContent=r&&r.message||`密码错误`,a.select())}s.addEventListener(`click`,u),a.addEventListener(`keydown`,e=>{e.key===`Enter`&&u()});function d(){t.remove()}return document.body.appendChild(t),setTimeout(()=>a.focus(),0),{destroy:d}}var c=`https://m1.apifoxmock.com/m1/6205743-5899102-default/zy-check-password`,l={cookieName:`zy_web_gate`,cookieValue:`1`,cookieDomain:void 0,maxAgeDays:7,sameSite:`Lax`,secure:void 0,title:`访问验证`,subtitle:`请输入访问密码后继续。`,placeholder:`访问密码`,buttonText:`进入`,timeoutMs:1e4};function u(e){let n={...l,...e||{}};return n.cookieDomain===void 0&&(n.cookieDomain=t()),n}function d(e){let t=u(e);return n(t.cookieName)===t.cookieValue?Promise.resolve():new Promise(e=>{s({title:t.title,subtitle:t.subtitle,placeholder:t.placeholder,buttonText:t.buttonText,onSubmit:async n=>{let i=await a(c,n,{timeoutMs:t.timeoutMs});return i.ok&&(r(t.cookieName,t.cookieValue,{domain:t.cookieDomain,maxAgeSeconds:t.maxAgeDays*24*60*60,sameSite:t.sameSite,secure:t.secure}),e()),i}})})}function f(e={}){let n={...l,...e},r=n.cookieDomain===void 0?t():n.cookieDomain;i(n.cookieName,{domain:r})}e.ensureGate=d,e.inferParentDomain=t,e.logoutGate=f,e.readCookie=n});
|
|
50
|
+
//# sourceMappingURL=zy-web-gate.umd.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"zy-web-gate.umd.cjs","names":[],"sources":["../src/cookie.js","../src/verify.js","../src/ui.js","../src/index.js"],"sourcesContent":["// 跨子域共享登录态的核心:父域 cookie 读写。\n//\n// 为什么必须用 cookie 而不是 localStorage:\n// localStorage 按 origin(scheme+host+port)隔离,a.example.com 写的东西\n// b.example.com 读不到,无法实现「一次验证、全子域通行」。\n// cookie 可以通过 Domain 属性设置到父域,被该域下所有子域共享读取。\n\n/**\n * 由当前 host 推断可用于跨子域共享的父域。\n *\n * 例如当前在 a.example.com,应返回 example.com,\n * 这样写入的 cookie 能被 my./b./x. 等所有兄弟子域读到。\n *\n * 注意:eu.org 在 Public Suffix List 上,浏览器禁止把 cookie 设到 eu.org,\n * 所以父域只能取到 example.com 这一层,刚好是我们要的、也是安全的。\n *\n * 这里采用「去掉最左一段」的朴素策略:a.example.com -> example.com。\n * 大多数「单层子域」场景够用;若有多级子域或想显式指定,用 options.cookieDomain 覆盖。\n *\n * @param {string} [hostname] 默认取 location.hostname\n * @returns {string} 用于 cookie Domain 的父域;localhost / IP 等场景返回空串(表示按当前 host)\n */\nexport function inferParentDomain(hostname) {\n const host = hostname || window.location.hostname;\n\n // localhost、单段主机名、IP 地址:不设 Domain,cookie 只对当前 host 生效。\n if (host === \"localhost\" || !host.includes(\".\")) return \"\";\n if (/^\\d{1,3}(\\.\\d{1,3}){3}$/.test(host)) return \"\";\n\n const parts = host.split(\".\");\n // 形如 example.com(两段):父域就是它自己。\n if (parts.length <= 2) return host;\n // 形如 a.example.com:去掉最左一段。\n return parts.slice(1).join(\".\");\n}\n\n/**\n * 读取指定名称的 cookie 值。\n * @param {string} name\n * @returns {string|null}\n */\nexport function readCookie(name) {\n const prefix = encodeURIComponent(name) + \"=\";\n const items = document.cookie ? document.cookie.split(\"; \") : [];\n for (const item of items) {\n if (item.indexOf(prefix) === 0) {\n return decodeURIComponent(item.slice(prefix.length));\n }\n }\n return null;\n}\n\n/**\n * 写入跨子域 cookie。\n *\n * @param {string} name\n * @param {string} value\n * @param {object} opts\n * @param {string} opts.domain cookie 的 Domain,传空串则不写 Domain(仅当前 host)\n * @param {number} opts.maxAgeSeconds 有效期(秒)\n * @param {string} [opts.sameSite=\"Lax\"]\n * @param {boolean} [opts.secure=true] HTTPS 下应为 true;本地 http 调试时会自动放宽\n * @param {string} [opts.path=\"/\"]\n */\nexport function writeCookie(name, value, opts) {\n const {\n domain,\n maxAgeSeconds,\n sameSite = \"Lax\",\n path = \"/\",\n } = opts;\n\n // 本地 http 调试时 Secure 会让 cookie 写不进去,这里按当前协议自动判断。\n const secure = opts.secure !== undefined\n ? opts.secure\n : window.location.protocol === \"https:\";\n\n let str =\n encodeURIComponent(name) + \"=\" + encodeURIComponent(value) +\n \"; path=\" + path +\n \"; max-age=\" + Math.floor(maxAgeSeconds) +\n \"; samesite=\" + sameSite;\n\n if (domain) str += \"; domain=\" + domain;\n if (secure) str += \"; secure\";\n\n document.cookie = str;\n}\n\n/**\n * 删除跨子域 cookie(用于「登出」)。Domain / Path 必须和写入时一致才能删掉。\n * @param {string} name\n * @param {object} opts\n * @param {string} opts.domain\n * @param {string} [opts.path=\"/\"]\n */\nexport function deleteCookie(name, opts) {\n const { domain, path = \"/\" } = opts;\n let str = encodeURIComponent(name) + \"=; path=\" + path + \"; max-age=0\";\n if (domain) str += \"; domain=\" + domain;\n document.cookie = str;\n}\n","// 把用户输入的密码发给校验接口,由接口判断对错。\n//\n// 设计要点(方案③):真密码只存在接口侧,前端 bundle 里没有任何密码信息。\n// 前端只发 { password },只收「对 / 错」。改密码只改接口,子站零改动。\n\n/**\n * 校验结果。\n * @typedef {object} VerifyResult\n * @property {boolean} ok 密码是否正确\n * @property {boolean} [networkError] 是否因网络/接口异常导致(区别于「密码错」)\n * @property {string} [message] 可展示给用户的提示\n */\n\n/**\n * 调用校验接口。\n *\n * 接口约定:\n * 请求 POST apiUrl body: {\"password\": \"用户输入\"}\n * 通过 HTTP 200, {\"code\":200, \"data\":{\"match\":true}}\n * 密码错 HTTP 401, {\"code\":401, \"data\":{}}(或任何 match !== true 的响应)\n *\n * 判定规则:只有「HTTP ok 且 data.match === true」才算密码正确;\n * 其余一律当密码错。网络层异常单独标记为 networkError,便于 UI 区分提示。\n *\n * @param {string} apiUrl\n * @param {string} password\n * @param {object} [opts]\n * @param {number} [opts.timeoutMs=10000]\n * @returns {Promise<VerifyResult>}\n */\nexport async function verifyPassword(apiUrl, password, opts = {}) {\n const { timeoutMs = 10000 } = opts;\n\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n\n let resp;\n try {\n resp = await fetch(apiUrl, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ password }),\n signal: controller.signal,\n });\n } catch (err) {\n clearTimeout(timer);\n // fetch 抛错只可能是网络层问题(断网、CORS、超时 abort 等),不是密码错。\n return {\n ok: false,\n networkError: true,\n message: \"网络异常,请稍后重试\",\n };\n }\n clearTimeout(timer);\n\n let data = null;\n try {\n data = await resp.json();\n } catch {\n // 响应不是合法 JSON:当作密码错/异常处理,不放行。\n data = null;\n }\n\n const match = !!(resp.ok && data && data.data && data.data.match === true);\n\n if (match) return { ok: true };\n\n return {\n ok: false,\n networkError: false,\n message: \"密码错误\",\n };\n}\n","// 框架无关的密码输入页(原生 DOM)。\n//\n// 用 Shadow DOM 把样式和结构完全隔离,既不会污染子站的 CSS,\n// 也不会被子站的全局样式影响。任何前端项目(Vue / React / 纯 HTML)都能用。\n\nconst STYLE = `\n:host { all: initial; }\n.mask {\n position: fixed; inset: 0; z-index: 2147483647;\n display: flex; align-items: center; justify-content: center;\n background: #0f1115;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"PingFang SC\", \"Microsoft YaHei\", sans-serif;\n}\n.card {\n width: 320px; max-width: calc(100vw - 48px);\n padding: 32px 28px;\n background: #1b1e25; border-radius: 14px;\n box-shadow: 0 12px 40px rgba(0,0,0,.45);\n box-sizing: border-box;\n}\n.title { margin: 0 0 6px; font-size: 18px; font-weight: 600; color: #f2f4f8; }\n.subtitle { margin: 0 0 22px; font-size: 13px; color: #8a92a6; line-height: 1.5; }\n.field { position: relative; }\n.input {\n width: 100%; box-sizing: border-box;\n padding: 11px 13px; font-size: 14px;\n color: #f2f4f8; background: #11141a;\n border: 1px solid #2c313c; border-radius: 9px; outline: none;\n transition: border-color .15s;\n}\n.input:focus { border-color: #4c8dff; }\n.btn {\n width: 100%; margin-top: 14px; padding: 11px 0;\n font-size: 14px; font-weight: 600; color: #fff; cursor: pointer;\n background: #4c8dff; border: none; border-radius: 9px;\n transition: background .15s, opacity .15s;\n}\n.btn:hover { background: #3a7df0; }\n.btn:disabled { opacity: .6; cursor: not-allowed; }\n.error {\n min-height: 18px; margin-top: 12px;\n font-size: 12.5px; color: #ff6b6b; line-height: 1.4;\n}\n`;\n\n/**\n * 创建并挂载密码页。返回一个对象,可用于销毁。\n *\n * @param {object} cfg\n * @param {string} cfg.title\n * @param {string} cfg.subtitle\n * @param {string} cfg.placeholder\n * @param {string} cfg.buttonText\n * @param {(password: string) => Promise<{ok: boolean, message?: string}>} cfg.onSubmit\n * 提交回调;返回 ok=true 时 UI 自动销毁,false 时显示 message。\n * @returns {{ destroy: () => void }}\n */\nexport function mountPasswordGate(cfg) {\n const host = document.createElement(\"div\");\n host.setAttribute(\"data-zy-web-gate\", \"\");\n const shadow = host.attachShadow({ mode: \"open\" });\n\n const style = document.createElement(\"style\");\n style.textContent = STYLE;\n shadow.appendChild(style);\n\n const mask = document.createElement(\"div\");\n mask.className = \"mask\";\n mask.innerHTML = `\n <div class=\"card\">\n <h1 class=\"title\"></h1>\n <p class=\"subtitle\"></p>\n <div class=\"field\">\n <input class=\"input\" type=\"password\" autocomplete=\"current-password\" />\n </div>\n <button class=\"btn\" type=\"button\"></button>\n <div class=\"error\" aria-live=\"polite\"></div>\n </div>\n `;\n shadow.appendChild(mask);\n\n // 用 textContent 赋值,避免把配置文本当 HTML 注入。\n shadow.querySelector(\".title\").textContent = cfg.title;\n shadow.querySelector(\".subtitle\").textContent = cfg.subtitle;\n const input = shadow.querySelector(\".input\");\n const btn = shadow.querySelector(\".btn\");\n const errorEl = shadow.querySelector(\".error\");\n input.placeholder = cfg.placeholder;\n btn.textContent = cfg.buttonText;\n\n let submitting = false;\n\n async function submit() {\n if (submitting) return;\n const password = input.value;\n if (!password) {\n errorEl.textContent = \"请输入密码\";\n return;\n }\n\n submitting = true;\n btn.disabled = true;\n errorEl.textContent = \"\";\n const originalBtnText = btn.textContent;\n btn.textContent = \"验证中…\";\n\n let result;\n try {\n result = await cfg.onSubmit(password);\n } finally {\n submitting = false;\n btn.disabled = false;\n btn.textContent = originalBtnText;\n }\n\n if (result && result.ok) {\n destroy();\n } else {\n errorEl.textContent = (result && result.message) || \"密码错误\";\n input.select();\n }\n }\n\n btn.addEventListener(\"click\", submit);\n input.addEventListener(\"keydown\", (e) => {\n if (e.key === \"Enter\") submit();\n });\n\n function destroy() {\n host.remove();\n }\n\n document.body.appendChild(host);\n // 自动聚焦输入框,方便直接打字。\n setTimeout(() => input.focus(), 0);\n\n return { destroy };\n}\n","// zy-web-gate —— 纯前端「共享固定密码」页面访问门。\n//\n// 用法(在 Vue/React/任意前端入口、挂载真实应用之前调用):\n//\n// import { ensureGate } from \"zy-web-gate\";\n//\n// await ensureGate(); // 校验接口已固定在包内,零参数即可\n//\n// // 走到这里说明已通过门禁,再启动真实应用:\n// createApp(App).mount(\"#app\");\n//\n// 跨子域:验证通过后写父域 cookie(Domain=example.com),同主域其他子站\n// 读到该 cookie 即直接放行,无需再次输入密码。\n// 安全边界:纯前端页面遮挡,防君子不防小人;真密码只在校验接口侧。\n\nimport { inferParentDomain, readCookie, writeCookie, deleteCookie } from \"./cookie.js\";\nimport { verifyPassword } from \"./verify.js\";\nimport { mountPasswordGate } from \"./ui.js\";\n\n// 校验接口固定在包内(写死、不可传)。\n// 它不是秘密——本来就出现在前端网络请求里;写死可让子站接入零参数。\n// 改地址时只改这里、发新版本,子站升级依赖即可。\nconst API_URL = \"https://m1.apifoxmock.com/m1/6205743-5899102-default/zy-check-password\";\n\n/** @type {Required<GateOptions>} 默认配置 */\nconst DEFAULTS = {\n cookieName: \"zy_web_gate\",\n cookieValue: \"1\",\n cookieDomain: undefined, // undefined 表示自动推断父域\n maxAgeDays: 7,\n sameSite: \"Lax\",\n secure: undefined, // undefined 表示按当前协议自动判断\n title: \"访问验证\",\n subtitle: \"请输入访问密码后继续。\",\n placeholder: \"访问密码\",\n buttonText: \"进入\",\n timeoutMs: 10000,\n};\n\n/**\n * @typedef {object} GateOptions\n * @property {string} [cookieName] 登录态 cookie 名,默认 \"zy_web_gate\"\n * @property {string} [cookieValue] 写入的 cookie 值,默认 \"1\"\n * @property {string} [cookieDomain] 显式指定父域;不传则自动推断(如 example.com)\n * @property {number} [maxAgeDays] 登录态有效天数,默认 7\n * @property {string} [sameSite] cookie SameSite,默认 \"Lax\"\n * @property {boolean} [secure] cookie Secure;不传则按当前协议自动判断\n * @property {string} [title] 密码页标题\n * @property {string} [subtitle] 密码页副标题\n * @property {string} [placeholder] 输入框占位文案\n * @property {string} [buttonText] 按钮文案\n * @property {number} [timeoutMs] 接口超时毫秒,默认 10000\n */\n\n/**\n * 解析最终配置。\n * @param {GateOptions} [options]\n */\nfunction resolveConfig(options) {\n const cfg = { ...DEFAULTS, ...(options || {}) };\n if (cfg.cookieDomain === undefined) {\n cfg.cookieDomain = inferParentDomain();\n }\n return cfg;\n}\n\n/**\n * 访问门主入口。\n *\n * 行为:\n * 1. 若父域 cookie 已存在有效登录态 -> 直接 resolve(放行,无 UI)。\n * 2. 否则弹出密码页,等用户输入并通过接口校验。\n * 3. 校验通过 -> 写父域 cookie -> resolve。\n *\n * 该 Promise 只在「已通过门禁」时 resolve;密码未通过时不会 resolve,\n * 因此把它放在 createApp().mount() 之前 await,可保证未通过时真实应用绝不挂载。\n *\n * @param {GateOptions} [options]\n * @returns {Promise<void>}\n */\nexport function ensureGate(options) {\n const cfg = resolveConfig(options);\n\n // 已有登录态:直接放行。\n if (readCookie(cfg.cookieName) === cfg.cookieValue) {\n return Promise.resolve();\n }\n\n return new Promise((resolve) => {\n mountPasswordGate({\n title: cfg.title,\n subtitle: cfg.subtitle,\n placeholder: cfg.placeholder,\n buttonText: cfg.buttonText,\n onSubmit: async (password) => {\n const result = await verifyPassword(API_URL, password, {\n timeoutMs: cfg.timeoutMs,\n });\n if (result.ok) {\n writeCookie(cfg.cookieName, cfg.cookieValue, {\n domain: cfg.cookieDomain,\n maxAgeSeconds: cfg.maxAgeDays * 24 * 60 * 60,\n sameSite: cfg.sameSite,\n secure: cfg.secure,\n });\n // 通过门禁:UI 会在本回调返回 ok 后自动销毁,这里同时 resolve 外层 Promise,\n // 让 await ensureGate() 的调用方继续往下挂载真实应用。\n resolve();\n }\n return result;\n },\n });\n });\n}\n\n/**\n * 主动登出:清除父域 cookie。调用后下次进入任一子站会重新要求输入密码。\n * @param {GateOptions} options 至少需与 ensureGate 一致的 cookieName / cookieDomain\n */\nexport function logoutGate(options = {}) {\n const cfg = { ...DEFAULTS, ...options };\n const domain = cfg.cookieDomain === undefined\n ? inferParentDomain()\n : cfg.cookieDomain;\n deleteCookie(cfg.cookieName, { domain });\n}\n\nexport { inferParentDomain, readCookie } from \"./cookie.js\";\n"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "zy-web-gate",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Framework-agnostic, pure-frontend shared-password page gate with cross-subdomain login state via a parent-domain cookie. No backend auth, no accounts — verification is delegated to a check-password API.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/zy-web-gate.umd.cjs",
|
|
7
|
+
"module": "./dist/zy-web-gate.js",
|
|
8
|
+
"unpkg": "./dist/zy-web-gate.umd.cjs",
|
|
9
|
+
"jsdelivr": "./dist/zy-web-gate.umd.cjs",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"import": "./dist/zy-web-gate.js",
|
|
13
|
+
"require": "./dist/zy-web-gate.umd.cjs"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"README.md"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"check": "node --check src/index.js && node --check src/cookie.js && node --check src/verify.js && node --check src/ui.js",
|
|
22
|
+
"build": "vite build",
|
|
23
|
+
"prepublishOnly": "npm run build"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"password",
|
|
27
|
+
"gate",
|
|
28
|
+
"auth-gate",
|
|
29
|
+
"static-site",
|
|
30
|
+
"cloudflare-pages",
|
|
31
|
+
"cross-subdomain",
|
|
32
|
+
"cookie",
|
|
33
|
+
"vue",
|
|
34
|
+
"frontend"
|
|
35
|
+
],
|
|
36
|
+
"author": "ZY",
|
|
37
|
+
"license": "ISC",
|
|
38
|
+
"sideEffects": false,
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"vite": "^8.0.16"
|
|
41
|
+
}
|
|
42
|
+
}
|