把认证流程交给服务端:Airbnb 如何减少 60% 认证代码

2026-09-04 35 预计阅读时间: 1 分钟
来源: infoq.com AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:11 分钟

认证系统很容易变成客户端里的条件判断迷宫:不同地区支持不同登录方式,高风险请求需要额外验证,新用户与老用户走不同路径,Web、iOS 和 Android 又分别实现一遍。Airbnb 将认证架构改造成服务端驱动流程,并通过策略选择验证挑战。根据来源摘要,这套 Flexible Authentication 系统减少了 60% 的认证相关代码,使 Web 客户端包缩小 100 KB,同时将认证成功率提高 2.6%、重复账户创建降低 27%,OTP 成本降低 11%。

真正值得关注的不是某一种登录协议,而是职责边界的变化:服务端决定“接下来验证什么”,客户端负责“如何把这一步呈现给用户”。

从客户端状态机转向服务端流程

传统认证页面通常把业务编排写在客户端:

如果用户存在且支持密码 -> 显示密码框
否则如果手机号可用 -> 发送短信 OTP
如果设备风险较高 -> 再要求第二种验证
如果账号疑似重复 -> 进入账号恢复流程

随着规则增加,这些判断会复制到多个客户端。一次策略调整可能需要修改三套应用、等待移动端审核,并长期兼容没有升级的旧版本。

服务端驱动架构把认证过程表示成一组语义化步骤。客户端提交当前上下文,服务端返回下一项挑战,例如:

{
  "flow_id": "flow_01J...",
  "status": "challenge_required",
  "challenge": {
    "type": "otp",
    "channel": "email",
    "destination_hint": "a***@example.com",
    "submit_url": "/v1/auth/flows/flow_01J.../answers"
  }
}

这里的关键是返回“OTP 挑战”这样的领域语义,而不是让服务端远程下发任意 UI。客户端仍然控制组件、无障碍体验、本地化和平台交互;服务端只控制流程顺序与策略结果。这样既能集中业务规则,也不会把客户端退化成不受约束的远程页面渲染器。

策略层如何选择挑战

策略选择可以综合账号状态、设备可信度、请求风险、地区能力和成本,但认证强度不能只由成本决定。一个合理的决策顺序可以是:

  1. 先满足安全底线,例如高风险登录必须使用更强的挑战。
  2. 在满足安全要求的方案中,优先选择用户已经配置且成功率更高的方式。
  3. 再考虑短信费用、送达率和平台能力。
  4. 为不可用的通道准备恢复路径,避免把用户锁死在流程中。

降低重复账户创建也与流程集中有关。服务端在注册之前可以统一执行账号发现、标识符归一化和恢复策略,而不是让每个客户端分别猜测用户应该“登录”还是“注册”。不过,对外响应必须避免暴露某个邮箱或手机号是否已经注册,否则会形成账号枚举漏洞。

来源摘要没有公开 Airbnb 的具体协议字段与策略实现。下面是一个可运行的简化示例,用来展示这种架构可以怎样实践,而不是对其内部 API 的复刻。

可运行的最小策略服务

将下面内容保存为 auth_server.py,使用 Python 3 直接运行。示例根据风险等级、可用通道和短信预算返回下一项挑战;生产系统还需要真实身份校验、持久化、签名令牌、限流与审计。

from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.parse import parse_qs, urlparse
import json
import uuid


def select_challenge(risk, has_passkey, sms_budget_ok):
    if risk == "high":
        return {"type": "passkey"} if has_passkey else {
            "type": "otp",
            "channel": "email"
        }

    if has_passkey:
        return {"type": "passkey"}

    if sms_budget_ok:
        return {"type": "otp", "channel": "sms"}

    return {"type": "otp", "channel": "email"}


class AuthHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        request = urlparse(self.path)
        if request.path != "/v1/auth/flow":
            self.send_error(404)
            return

        query = parse_qs(request.query)
        risk = query.get("risk", ["low"])[0]
        has_passkey = query.get("passkey", ["false"])[0] == "true"
        sms_budget_ok = query.get("sms_budget", ["true"])[0] == "true"

        flow_id = str(uuid.uuid4())
        payload = {
            "schema_version": 1,
            "flow_id": flow_id,
            "status": "challenge_required",
            "challenge": select_challenge(
                risk, has_passkey, sms_budget_ok
            ),
            "submit_url": f"/v1/auth/flows/{flow_id}/answers"
        }
        body = json.dumps(payload).encode("utf-8")

        self.send_response(200)
        self.send_header("Content-Type", "application/json")
        self.send_header("Cache-Control", "no-store")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)


if __name__ == "__main__":
    server = HTTPServer(("127.0.0.1", 8080), AuthHandler)
    print("Listening on http://127.0.0.1:8080")
    server.serve_forever()

启动服务并请求两种上下文:

python3 auth_server.py

curl -s 'http://127.0.0.1:8080/v1/auth/flow?risk=low&passkey=true' | python3 -m json.tool
curl -s 'http://127.0.0.1:8080/v1/auth/flow?risk=high&passkey=false&sms_budget=false' | python3 -m json.tool

第一个请求会选择 passkey;第二个请求在高风险且没有 passkey 时选择邮件 OTP。实际客户端应维护一个明确的挑战类型注册表,例如只接受 passkeyotpaccount_recovery。遇到未知类型时,应显示可恢复的兼容提示并上报遥测,而不是忽略挑战或直接放行。

指标改善为什么可能同时发生

这组结果并非互相独立。服务端集中规则后,客户端删除重复的状态机和实验分支,因此代码量与 Web 包体积下降。策略可以更快地调整挑战顺序,减少无效步骤,从而改善认证成功率。账号发现与恢复逻辑统一后,用户误入注册流程的概率下降,也就减少了重复账号。

OTP 成本下降通常不能简单理解为“少发短信”。更可靠的做法包括优先使用已经注册的 passkey、避免重复发送、为请求设置幂等键、根据送达情况选择通道,并阻止攻击者滥用验证码接口。否则,成本优化可能以降低可达性或安全性为代价。

评估这类改造时,至少应同时观察:

  • 流程开始率、挑战完成率和端到端认证成功率。
  • 按平台、地区、客户端版本与挑战类型拆分的失败率。
  • OTP 发送量、重发率、送达率及单次成功认证成本。
  • 账号恢复成功率、重复账号率和账号枚举风险。
  • 未知挑战类型、流程超时和策略回退的发生次数。

落地时守住几个边界

服务端驱动认证适合规则变化快、客户端众多、验证方式不断扩展的系统,但它也把更多可用性责任集中到了认证服务。采用时应重点检查以下事项:

  • 版本兼容:响应包含 schema 版本,服务端只向旧客户端发送其声明支持的挑战。
  • 流程完整性flow_id 应使用服务端状态或经过签名、短时有效的令牌,防止用户篡改步骤与重放答案。
  • 幂等与限流:发送 OTP、提交答案和创建账号都要有幂等控制,并按账号、设备、IP 和风险信号限流。
  • 安全失败模式:未知挑战、策略服务超时或依赖故障时不能绕过验证;同时要提供可解释的恢复入口。
  • 渐进发布:先以影子决策比较新旧策略,再按客户端版本或小比例流量启用,并为成功率和风险指标设置自动回滚阈值。

60% 的代码削减很醒目,但更重要的结果是认证规则有了单一决策入口。客户端因此可以专注交互质量,安全与增长团队也能在同一套策略和指标上迭代。对于准备采用这一模式的团队,最稳妥的起点不是一次性重写全部登录页面,而是先抽出一个高变动流程,例如 OTP 通道选择或登录与注册分流,验证协议兼容、可观测性和回滚能力后再扩大范围。


相关推荐