Applogin — 统一登录
只需一段脚本,即可为您的应用添加 Google 和邮箱/密码登录。全部认证 — 登录、注册、邮箱验证、密码重置、Google — 都在 applogin.one 上受保护的 iframe 内完成。您的页面永远看不到用户密码,也不做任何校验 — 您只会收到一个开箱即用的应用专属令牌。
- 渲染登录/注册表单(在其自己的 iframe 中)— 您无需自己构建。
- 接收密码、校验凭证、发送并验证邮箱验证码 — 您什么都不用验证。
- 每个邮箱只维护一个账号(Google 和密码指向同一账号)。
- 维护跨应用会话,在您的多个应用之间自动接续登录。
工作原理
Applogin 是一个认证提供方(类似 Auth0/Clerk),但登录表单是从我们的域名以 frame 形式提供的。您的应用通过 SDK 以模态框打开该 frame,并通过 postMessage 接收结果。令牌是用您的应用密钥签名的 JWT(aud=app_id),在任何其他应用中都无效。
| 概念 | 说明 |
|---|---|
| 应用 (app) | 平台上的实体。拥有公开的 app_id、jwt_secret(后端令牌验证)以及 allowed_origins / redirect_uris 列表。 |
| 终端用户 | 平台所有应用共享的全局账号(每个邮箱一个)。Google 和密码关联到同一账号。 |
| 应用令牌 | 用您应用的 jwt_secret 签名的 JWT(HS256,aud=app_id)。背后是可吊销的会话。 |
| SSO 会话 | 存于 applogin.one 上 HttpOnly+Partitioned Cookie al_sso 中的全局会话。在同一顶级站点内共享。 |
快速开始
引入两个脚本并挂载组件 — 它会静默尝试 SSO,显示"登录"按钮,在 frame 中打开托管表单并返回用户。
<script src="https://applogin.one/applogin.js?v=1"></script>
<script src="https://applogin.one/applogin-ui.js?v=1"></script>
<script>
ApploginUI.mount({
appId: "app_your_id",
onAuth: (user) => renderApp(user), // signed in: { id, email, name, picture, email_verified }
onLogout: () => showLanding()
});
// sign-out button: ApploginUI.logout();
</script>
就这些。表单、注册、邮箱验证码、Google 和密码重置全部由 Applogin 处理。唯一的要求是注册一个 app_id 并配置您的 allowed_origins(见创建应用)。
?v=1 版本号可防止浏览器在更新后返回缓存的旧版 SDK。ApploginUI 组件 推荐
为您的应用准备的现成"门锁":未登录时显示带"登录"按钮的屏幕;点击后在模态框中打开托管表单;登录后调用 onAuth(user) 并移除屏幕。
| 方法 | 说明 |
|---|---|
ApploginUI.mount({ appId, onAuth, onLogout, title? }) | 挂载。静默尝试 SSO;未登录则渲染"登录"屏幕。 |
ApploginUI.logout() | 退出登录(吊销会话,重新显示屏幕)。 |
ApploginUI.getUser() | 当前用户或 null。 |
个人资料(头像、姓名、邮箱)和退出按钮由您在 onAuth(user) 中自行渲染 — 那是您的 UI。用户数据到手即可使用。
SDK 方法(applogin.js)低层级
如果您想要自己的 UX 而不是组件,可直接使用 SDK。先调用 Applogin.init({ appId })。令牌保存在 localStorage 的 al_token_<appId> 下。
| 方法 | 说明 | 返回 |
|---|---|---|
init({ appId }) | 初始化。 | — |
getSession() | 静默 SSO:若用户已在同一站点的其他应用登录,则接续会话。 | { authenticated, token, user } |
openLogin() | 打开托管登录表单模态框(applogin.one 上的 iframe)。 | { token, user, lang } 或 null |
verify() | 验证当前令牌。 | { valid, user } |
logout() | 退出(吊销会话并清除令牌)。 | — |
getToken() | 当前应用令牌。 | string | null |
Applogin.init({ appId: "app_xxx" });
// on load — silently try SSO:
const s = await Applogin.getSession();
if (s.authenticated) showApp(s.user);
else {
const r = await Applogin.openLogin(); // modal with the applogin.one form
if (r) showApp(r.user);
}
// attach Applogin.getToken() to your backend requests
单点登录(SSO)
登录时,Applogin 会设置带 Partitioned 属性的 Cookie al_sso。分区键是顶级站点(最外层)。因此:
- 同一站点下的多个应用(例如嵌入在
appdock.pro中)共享会话 → 第二个应用的getSession()无需表单即可接续登录。 - 同一应用作为独立站点打开(另一个顶级站点)则是另一个分区 → 用户需重新登录。
const s = await Applogin.getSession(); // ← {authenticated:true,…} if a session exists in this partition
if (s.authenticated) showApp(s.user);
allowed_origins 必须同时列出其自身域名和外层包装域名(例如 appdock.pro)。allowed_origins 把关(无关站点永远拿不到令牌)。语言
托管登录表单支持 10 种语言:English, Deutsch, Français, Español, Italiano, Português, 日本語, 한국어, 中文, Русский。默认按用户的浏览器语言打开(回退为英语);用户可直接在表单中切换语言 — 底部的国旗菜单。
- 指定语言。向组件或 SDK 传入
lang:ApploginUI.mount({ appId, lang: "zh" })或Applogin.init({ appId, lang: "zh" })— 表单将以该语言打开。不传时,SDK 会自动发送浏览器语言。 - 读取用户的选择。用户可能在表单内切换语言。
Applogin.openLogin()的结果包含lang(登录时表单所处的语言),您的页面可以随之切换。组件也会将其保存到 localStorage 的al_lang,下次表单将以该语言打开。 - 邮件跟随表单。验证码和密码重置邮件按表单语言发送。
底层上,表单 URL 接受 ?lang=xx(上表中的两位代码)— SDK 会替您传递。
在线示例:AppDock
appdock.pro 是一个"坞站"式启动器:一个页面通过 iframe 打开多个应用。应用各自运行在自己的域名上:
| 应用 | 域名 | app_id |
|---|---|---|
| Email Writer | emailwriter.online | app_emailwriter |
| Audio Recorder | audiorecorder.info | app_recorder |
在坞站内登录 Email Writer,然后打开(同样在坞站内的)Audio Recorder — 登录会自动接续:二者都运行在同一顶级站点 appdock.pro 之下,共享一个 SSO 分区。
1. 坞站启动器(appdock.pro)
<button class="tile" data-src="https://emailwriter.online/">Email Writer</button>
<button class="tile" data-src="https://audiorecorder.info/">Audio Recorder</button>
<iframe id="frame" allow="microphone; clipboard-write"></iframe>
<script>
document.querySelectorAll(".tile").forEach(t =>
t.onclick = () => document.getElementById("frame").src = t.dataset.src);
</script>
2. 应用(例如 emailwriter.online)
<script src="https://applogin.one/applogin.js?v=1"></script>
<script src="https://applogin.one/applogin-ui.js?v=1"></script>
<script>
ApploginUI.mount({
appId: "app_emailwriter",
title: "Email Writer — sign in",
onAuth: (user) => { // render the profile + the app
header.textContent = user.name + " · " + user.email;
app.hidden = false;
},
onLogout: () => { app.hidden = true; }
});
</script>
3. 此场景下的应用注册
每个应用的 allowed_origins 都列出自身域名 + 坞站域名(frame-ancestors 会看到嵌套 frame 的整条祖先链):
app_emailwriter → allowed_origins: ["https://emailwriter.online", "https://appdock.pro"]
app_recorder → allowed_origins: ["https://audiorecorder.info", "https://appdock.pro"]
// redirect_uris (for Google): ["https://applogin.one/auth-callback.html"]
ApploginUI.mount。在您的后端验证令牌
前端将 Applogin.getToken() 放入请求的 Authorization 头。后端有两种验证方式。
1. 通过平台端点 考虑吊销
POST https://applogin.one/api/v1/session/verify
{ "app_id": "app_xxx", "token": "<JWT from Authorization>" }
// → { "valid": true, "user": { "id": 2, "email": "…", "email_verified": true, "name": "…", "picture": null } }
// invalid/revoked (logout) → { "valid": false, "user": null }
2. 本地验证(用您的 jwt_secret 做 HS256)无需网络
// Node.js
const jwt = require("jsonwebtoken");
const claims = jwt.verify(token, process.env.APPLOGIN_JWT_SECRET, {
algorithms: ["HS256"], audience: "app_xxx", issuer: "applogin"
});
// claims.sub — user id, claims.email, claims.email_verified
# Python
import jwt
claims = jwt.decode(token, APPLOGIN_JWT_SECRET, algorithms=["HS256"],
audience="app_xxx", issuer="applogin")
/v1/session/verify。JWT 结构
{
"iss": "applogin",
"aud": "app_xxx", // your app_id
"sub": "2", // user id (as a string)
"jti": "…", // session id (for revocation)
"email": "user@example.com",
"email_verified": true,
"name": "Jane", "picture": null,
"iat": 1782194619, "exp": 1784786619 // ~30 days
}
Webhook(可选)
如果应用设置了 webhook_url,平台会在每次 Google 登录成功时向其发送带有已验证身份的签名 POST — 在令牌之外用于服务端同步。
| 请求头 | 值 |
|---|---|
X-Applogin-Timestamp | unix-ms |
X-Applogin-Signature | timestamp + "." + rawBody 的 sha256=<hex> |
const exp = "sha256=" + crypto.createHmac("sha256", WEBHOOK_SIGNING_SECRET)
.update(ts + "." + rawBody).digest("hex");
if (!crypto.timingSafeEqual(Buffer.from(exp), Buffer.from(sig))) reject();
创建应用
应用在私有控制台 applogin.one/dashboard 中创建(用 Applogin 自身登录)。您需要提供:
| 字段 | 用途 |
|---|---|
name | 显示在登录表单中("登录 · <name>")。 |
allowed_origins | 允许嵌入表单并接收令牌的来源。列出应用域名 + 外层包装域名(例如坞站)。 |
redirect_uris | 用于 Google 登录:https://applogin.one/auth-callback.html。 |
webhook_url | (可选)登录 Webhook 的接收地址。 |
您将获得:app_id(公开)、jwt_secret(后端令牌验证)、client_secret、webhook_signing_secret。
REST API — 参考
基址:https://applogin.one/api。以下是开发者真正需要的。邮箱/密码和 Google 交换端点由托管表单自己调用 — 您永远不用调用。
供开发者使用
在后端验证应用令牌(考虑吊销)。{ app_id, token } → { valid, user }。
应用的公开数据(name、allowed_origins)。由托管登录页使用。
内部使用(由托管表单/SDK 调用,不是您)
| 端点 | 角色 |
|---|---|
POST /v1/email/register · verify · resend · login · reset/request · reset/confirm · password/change | 邮箱/密码 — 由托管表单 /login 调用。 |
POST /v1/auth/start, GET /oauth/google/callback, POST /v1/auth/exchange | Google OAuth(在托管表单内部)。 |
POST /v1/session/sso | 静默 SSO(由隐藏 iframe 中的 /session 调用)。 |
POST /v1/session/establish | 在正确的分区中设置 SSO Cookie(Google 之后)。 |
POST /v1/session/logout | 吊销会话(通过 SDK logout())。 |
错误
| HTTP | 场景 |
|---|---|
| 400 | 输入无效(密码过短、验证码无效/过期、"Email already registered")。 |
| 403 | "Invalid credentials"、拒绝访问、"Too many attempts"(限流)。 |
| 404 | 未知的应用/路由。 |
{"error":"Invalid or expired code","message":"Invalid or expired code"}
安全性
- 密码被隔离。输入框只存在于 applogin.one 的 iframe 内;嵌入站点的 JS 无法触及。开发者无法"窃听"密码。
- 应用专属令牌。JWT 用应用密钥签名并携带
aud=app_id— 一个应用的令牌在另一个应用中无效。 - 令牌只投递给您的来源。表单只通过
postMessage向allowed_origins中的来源发送令牌;登录页本身带Content-Security-Policy: frame-ancestors <allowed_origins>提供(防点击劫持)。 - 全局会话是 HttpOnly。Cookie
al_sso对 JS 不可见;开发者手中只有应用令牌。 - SSO 的 CSRF 防护。
/v1/session/sso仅当Origin ∈ allowed_origins时才发放令牌 — 持有用户 Cookie 的无关站点什么也拿不到。 - 防暴力破解。login/reset/verify 有限流(按邮箱和 IP)。
- 密码经哈希存储(PBKDF2);验证码哈希存储、TTL 10 分钟、限制尝试次数;全程 HTTPS;从不存储 Google 令牌。Google 登录使用 PKCE(S256)+ state + nonce。
LLM 速查表
模型:所有认证都发生在 applogin.one 的 iframe 中。开发者不渲染表单、不发送密码、不校验验证码。开发者引入 SDK + 组件并接收应用令牌。
// include
<script src=https://applogin.one/applogin.js?v=1></script>
<script src=https://applogin.one/applogin-ui.js?v=1></script>
// mount (a lock on the app)
ApploginUI.mount({ appId, onAuth:(user)=>..., onLogout:()=>... });
ApploginUI.logout();
// low level
Applogin.init({ appId });
await Applogin.getSession(); // {authenticated, token, user} — silent SSO
await Applogin.openLogin(); // {token, user, lang}|null — modal with the form
Applogin.getToken(); await Applogin.verify(); await Applogin.logout();
// backend: verify the token
POST https://applogin.one/api/v1/session/verify {app_id, token} -> {valid, user}
// or locally: jwt.verify(token, jwt_secret, {algorithms:["HS256"], audience:app_id, issuer:"applogin"})
SSO:登录会设置 Cookie al_sso(Partitioned,按顶级站点分区)。同一站点下的应用(例如 appdock.pro 这类坞站的 iframe)共享会话;独立域名 → 重新登录。应用的 allowed_origins = 自身域名 + 外层包装域名。
令牌为应用专属:iss=applogin、aud=app_id、sub=用户 ID,用应用的 jwt_secret 签名。