zy-web-gate
v3.1.0
Published
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.
Maintainers
Readme
zy-web-gate
纯前端「共享固定密码」页面访问门。框架无关,任何前端项目(Vue / React / 纯 HTML)都能用。
输入正确的共享密码后才能看到页面内容;验证一次后,同主域下所有子站自动放行,无需重复输入。新增子站只要接入本包即可纳入同一套门。
它是什么 / 不是什么
这是一道前端页面层的访问遮挡,属于「防君子不防小人」:
- ✅ 没输对密码看不到页面、输对能进、同主域跨子域不用重复输。
- ✅ 真密码只存在校验接口侧,前端 bundle 里没有任何密码信息(连 hash 都没有)。
- ✅ 登录态 cookie 存的是后端签发的 JWT,每次进入调
/check验签——能挡住「控制台伪造 cookie 直接进 UI」与「照搬旧=1绕过」。 - ❌ 不防 DevTools、不防绕过 UI 直接扒静态资源 / 调业务 API。
- ❌ 不是账号体系、不是 OAuth、不是邮箱/短信验证码,就是「一个共享固定密码」。
如果你需要真正的安全鉴权,请用后端鉴权 / 身份系统,本包不适用。
工作原理
先用一个固定 token 向「地址分发接口」换取真实校验地址,再去真实地址校验,避免真实接口地址被写死在 bundle 里。
- 进入任一子站时,先换真实地址、调
/site-status问「当前网站受控吗」(后端按 Origin 判断)。不受控 → 直接放行,连密码框都不弹。 - 受控 → 查父域 cookie(
Domain=example.com,值为 JWT)。 - cookie 存在 → 调
/check验签;验过才放行,不弹任何 UI。 - cookie 不存在 / 验签不过 → 弹出密码输入页(原生 DOM,Shadow DOM 隔离样式)。
- 用户输入密码 →
POST {url}/verify→ 接口比对后返回「对 / 错」并在对时签发 JWT。 - 正确 → 把 JWT 写父域 cookie → 放行;之后所有同主域子站读到该 cookie 并验签通过即免输。
为什么用 cookie 而不是 localStorage:localStorage 按 origin 隔离,子域之间读不到,无法实现「一次验证、全子域通行」。cookie 可通过 Domain 设置到父域被全部子域共享,这是纯前端实现跨子域登录态的唯一可靠机制。
安装
npm install zy-web-gate自 1.1.0 起自带 TypeScript 类型声明(dist/index.d.ts),TS 项目无需手写 .d.ts。
使用
在挂载真实应用之前调用 ensureGate({ env })。它只在「已通过门禁」时 resolve,所以未通过时真实应用绝不会挂载(无内容闪现)。
env必传(取值dev/test):地址分发接口按env返回对应环境的真实校验地址。不传或传非法值会立即抛错,不做自动探测。
Vue3 + Vite
// main.js
import { createApp } from "vue";
import App from "./App.vue";
import { ensureGate } from "zy-web-gate";
// 用构建 mode 区分环境(dev 构建传 dev,test 构建传 test)
await ensureGate({ env: import.meta.env.MODE });
createApp(App).mount("#app");顶层
await需要入口是 ESM module(Vite 默认满足)。若环境不支持顶层 await,用.then():ensureGate({ env: import.meta.env.MODE }).then(() => { createApp(App).mount("#app"); });
React
import { ensureGate } from "zy-web-gate";
ensureGate({ env: import.meta.env.MODE }).then(() => {
ReactDOM.createRoot(document.getElementById("root")).render(<App />);
});纯 HTML(通过 CDN,无需打包工具)
用 UMD 产物,全局变量为 ZyWebGate。无构建工具时按部署环境手动指定 env:
<script src="https://unpkg.com/zy-web-gate"></script>
<script>
ZyWebGate.ensureGate({ env: "test" }).then(function () {
document.getElementById("app").style.display = "";
});
</script>或用 ESM 方式:
<script type="module">
import { ensureGate } from "https://unpkg.com/zy-web-gate?module";
await ensureGate({ env: "test" });
document.getElementById("app").style.display = "";
</script>校验接口约定(地址分发 + 站点状态 + JWT)
地址分发接口写死在包内(src/discover.js),真实校验地址由分发接口动态返回。完整字段、示例与安全说明见 INTEGRATION.md。
地址分发(换真实地址)
| | |
|---|---|
| 方法 | GET |
| URL | src/discover.js 的 DISCOVER_URL(写死在包内) |
| 必带请求头 | token + env(按 ensureGate({ env }) 取,dev/test 各一套,写死在包内;非安全凭证,仅提高扒取门槛) |
| 成功 | HTTP 200,{ "code": 200, "data": { "url": "真实校验地址" } } |
分发接口按
env头返回对应环境的真实校验地址。改真实地址:改分发接口(mock)的返回值即可,无需发新版本;改分发接口本身的地址、token 或环境才需改src/discover.js发新版本。本包每次校验都重新分发拉取,地址变更即时生效。
站点状态查询(弹框前先问:当前网站受控吗)
| | |
|---|---|
| 方法 | GET |
| URL | {真实地址}/site-status |
| 返回 | HTTP 200,{ "code": 0, "data": { "controlled": true \| false } } |
ensureGate() 在弹密码框之前先调一次:后端按请求 Origin 判断「当前网站」是否受访问门控制。只有明确返回 controlled === false 才直接放行(连密码框都不弹);返回 true、接口不存在或网络异常一律按受控处理,继续走下方门禁流程。
密码校验
| | |
|---|---|
| 方法 | POST |
| URL | {真实地址}/verify |
| 请求体 | { "password": "用户输入的密码" } |
| 通过 | HTTP 200,{ "code": 0, "data": { "match": true, "token": "<JWT>" } } |
| 密码错 | 任何 data.match !== true 的响应(HTTP 401 也可) |
| 无权限 | 密码正确但对当前网站无授权:data.match !== true 且带 message(如 HTTP 403,{ "code": 1, "message": "当前访问密码无权限访问当前网站", "data": { "match": false, "reason": "no_permission" } }) |
判定规则:只有「HTTP ok 且 data.match === true」算通过,其余一律当未通过;通过时把 data.token 写入 cookie。未通过时,若响应体带 message 字段则原样展示给用户(用于「无权限访问」等场景),否则提示「密码错误」;网络/接口异常单独提示「网络异常」。
后端可按请求
Origin(浏览器跨域自动携带、前端不可伪造)识别「当前网站」,从而实现「某密码只能用于某些网站」「某网站不受门控直接放行」等策略。本包无需为此传任何站点参数。
token 验签(已有 cookie 时)
| | |
|---|---|
| 方法 | POST |
| URL | {真实地址}/check |
| token 位置 | 请求头 Authorization: Bearer <token>(同时也放进 body) |
| 有效 | HTTP 200,{ "code": 0, "data": { "valid": true } } |
接口需放行 CORS(响应带
Access-Control-Allow-Origin,并正确处理OPTIONS预检),否则前端跨域调不通。建议接口侧加限流防暴力猜密码。
配置项
ensureGate(options):除 env 外其余均可选。
| 选项 | 默认 | 说明 |
|---|---|---|
| env | 必传 | 环境名,取值 dev / test,用于地址分发接口区分环境。缺失/非法立即抛错 |
| cookieName | "zy_web_gate" | 登录态 cookie 名(值为后端签发的 JWT,不可配) |
| cookieDomain | 自动推断 | 父域;不传则由当前 host 推断(如 a.example.com → example.com)。localhost / IP 自动不写 Domain |
| maxAgeDays | 7 | 登录态有效天数 |
| sameSite | "Lax" | cookie SameSite |
| secure | 自动 | 按当前协议自动判断(https 为 true,本地 http 自动关闭) |
| title | "访问验证" | 密码页标题 |
| subtitle | "请输入访问密码后继续。" | 副标题 |
| placeholder | "访问密码" | 输入框占位 |
| buttonText | "进入" | 按钮文案 |
| loadingText | "正在验证访问权限…" | 已有 cookie 验签时的全屏 loading 文案(避免慢网白屏) |
| timeoutMs | 10000 | 接口超时(毫秒) |
登出
import { logoutGate } from "zy-web-gate";
logoutGate(); // 清除父域 cookie,下次进入任一子站会重新要求输入密码若
ensureGate用了自定义cookieName/cookieDomain,logoutGate要传相同的值才能删掉对应 cookie。
跨子域 / 父域说明
- 父域自动推断为「去掉最左一段」:
a.example.com→example.com。多级子域或想固定时,显式传cookieDomain。 - 浏览器禁止把 cookie 设到 Public Suffix List 上的公共后缀(如
com、eu.org等),所以父域只会落到你自己的可注册域那一层——这正是我们要的,也避免 cookie 泄漏给同后缀下别人的站点。 - 全站需 HTTPS(Cloudflare Pages 默认满足),
Securecookie 才能写入。
新增子站如何纳入
纯前端方案没有「零接入自动保护」——每个子站都要接入本包(装包 + 入口 await ensureGate({ env }) 几行)。但因登录态是全子域共享 cookie,新站只要接入了,已验证用户进去不会再被拦。
校验接口已内置在包内,新站接入只需装包 + 入口调一行 await ensureGate({ env }),可做成共享模板 / 脚手架复制即用。
本地验证
cd examples
npx serve . # 或 python3 -m http.server
# 浏览器打开 demo.html本地 localhost 下 cookie 不写 Domain、Secure 自动关闭,可验证「输对进、输错拦、刷新免输」;跨子域共享需部署到真实 *.example.com 才能验证。
构建与发布
源码在 src/,发布前用 Vite 库模式构建出 dist/(ESM + UMD):
npm run build # 产出 dist/zy-web-gate.js (ESM) 和 dist/zy-web-gate.umd.cjs (UMD)
npm publish # prepublishOnly 会自动先 buildpackage.json 的 files 只包含 dist 和 README.md,源码与示例不进 npm 包。
