背景与价值
Magic Link降低密码负担,但若无签名与一次性治理易被滥用。统一签名、白名单与窗口策略可保障安全。
统一规范
- 一次性:链接含 `nonce`,验证后立即作废。
- 过期窗口:`exp` 不超过15分钟,过期拒绝。
- 来源白名单:链接跳转 `origin` 必须受控。
核心实现
签名生成与校验
```ts
function enc(s: string): Uint8Array { return new TextEncoder().encode(s) }
async function importHmac(secret: ArrayBuffer): Promise { return crypto.subtle.importKey('raw', secret, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']) }
async function hmac(key: CryptoKey, data: string): Promise { const raw = await crypto.subtle.sign('HMAC', key, enc(data)); const u = new Uint8Array(raw); let s=''; for (let i=0;i()
const allowOrigins = new Set(['https://app.example.com'])
function genId(): string { return Math.random().toString(36).slice(2) }
function now(): number { return Date.now() }
async function issueLink(email: string, key: CryptoKey, ttlMs = 900000): Promise { const t: Omit = { email, nonce: genId(), exp: now() + ttlMs }; const payload = `${t.email}|${t.nonce}|${t.exp}`; const sig = await hmac(key, payload); store.add(t.nonce); return { ...t, sig } }
function originAllowed(url: string): boolean { try { const u = new URL(url); return allowOrigins.has(u.origin) } catch { return false } }
async function verifyLink(t: Token, key: CryptoKey, redirect: string): Promise { if (!originAllowed(redirect)) return false; if (!store.has(t.nonce)) return false; if (now() > t.exp) return false; const payload = `${t.email}|${t.nonce}|${t.exp}`; const expect = await hmac(key, payload); const ok = expect === t.sig; if (ok) store.delete(t.nonce); return ok }
```
落地建议
- 对链接实行一次性与过期窗口治理,并绑定跳转来源白名单。
- 审计签发与验证事件,失败立即告警并阻断。
验证清单
- `nonce` 是否一次性使用且过期后拒绝。
- 跳转 `origin` 是否命中白名单,签名是否匹配。
发表评论 取消回复