加载中...
  • 登录之外的认证体系:账号、密码、JWT、OAuth 与资源隔离loading

    AI 应用后端工程化:从原型到可交付系统 · 第 16 篇 · 第四章 · 会话与安全

    从账号模型到密码哈希、令牌解析、第三方登录和资源所有权,建立一条完整认证链。

    增加登录页面不等于拥有认证体系。账号如何保存,密码如何验证,Token 怎样失效,第三方 OAuth 返回什么身份,以及用户能否读取不属于自己的数据,这些环节必须连成一条链。

    最重要的区分是:认证证明“你是谁”,授权判断“你能做什么”。JWT 解析成功只能得到身份,不能自动证明该用户拥有某个数据集。

    一条请求经过的安全路径

    flowchart LR
      Login[密码或 OAuth 登录] --> Verify[验证身份]
      Verify --> Token[签发访问令牌]
      Token --> Middleware[请求中间件解析]
      Middleware --> Principal[当前主体]
      Principal --> Service[业务服务]
      Resource[(资源记录)] --> Owner[所有权/角色检查]
      Service --> Owner
      Owner -->|允许| Action[执行动作]
      Owner -->|拒绝| Deny[403]
    

    中间件负责验证签名、过期时间和签发方,构造当前主体。业务服务根据主体与目标资源判断权限。把所有权检查只写在前端或 Handler 中,后台任务和其他入口很容易绕过。

    密码只保存慢哈希

    密码不能明文存储,也不应使用通用快速哈希。应使用 Argon2、bcrypt 等专用算法,每个密码拥有独立盐,并允许未来提升成本参数。

    def authenticate(account, password: str, hasher) -> bool:
        if account.disabled_at is not None:
            return False
        if not hasher.verify(password, account.password_hash):
            return False
        if hasher.needs_rehash(account.password_hash):
            account.password_hash = hasher.hash(password)
        return True
    

    登录失败提示不要区分“账号不存在”和“密码错误”,减少枚举风险。接口要限速并记录异常模式,高风险场景增加二次验证。日志绝不记录密码和完整 Token。

    JWT 不是加密保险箱

    常见 JWT 只签名不加密,Payload 可以被任何拿到 Token 的人解码。里面只放必要的主体 ID、签发时间、过期时间、签发方和用途,不放密码、OAuth access token 或隐私资料。

    访问 Token 生命周期要短,刷新 Token 单独管理并支持撤销。密钥轮换使用 key ID 区分新旧签名;验证时限制允许算法,不能盲目信任 Token Header。修复“生成函数忘记返回 Token”这类问题也提醒我们:安全组件同样需要最基础的成功与失败测试。

    OAuth 解决委托,不替代账号模型

    第三方登录返回的是某个 Provider 中的身份。系统仍需要本地账号,把 provider + subject 映射到它。不要只用邮箱自动合并,除非确认 Provider 已验证邮箱并设计了冲突策略。

    OAuth 授权流程要验证 state 防止请求伪造,使用 PKCE 保护授权码,回调地址固定白名单。第三方 access token 与本站会话 Token 是两种凭证,权限和生命周期不能混淆。

    资源隔离发生在每次查询

    最常见越权漏洞是根据 URL 中的 ID 直接 get(id)。攻击者把 ID 改成别人的值,若 Service 没有所有者条件就会返回数据。

    更稳妥的仓储接口直接包含主体条件,例如 get_dataset(dataset_id, account_id);管理员能力使用单独方法并记录审计。创建子资源时也要检查父资源归属,不能只验证子资源提交里的 account ID。

    多租户系统需要租户边界,个人身份不能跨租户推导权限。后台任务携带主体或明确服务身份,避免因为“不是 HTTP 请求”就跳过授权。

    失败码要准确

    未提供或无效凭证返回 401,身份有效但没有权限返回 403,目标不存在返回 404。为防止资源枚举,有时越权也统一返回 404,但内部审计仍要区分。稳定错误协议帮助客户端决定重新登录、提示权限不足还是停止重试。

    一个危险实现会在 JWT 中放 is_admin=true,之后管理员被降权,旧 Token 在过期前仍可使用。高风险权限可以每次查询数据库或使用短 Token 与权限版本号,取舍取决于一致性要求。

    威胁模型比组件清单重要

    账号、JWT、OAuth 都存在并不代表安全。要沿攻击路径检查:暴力登录、Token 泄露、重放、跨用户 ID、OAuth 账号绑定和日志泄密。每条路径至少有确定性控制和测试。

    练习:创建两个账号和各自的数据集,验证用户 A 对用户 B 的查询、更新和删除全部失败;再让 A 的 Token 过期、账号禁用和权限版本变化,记录系统行为。最后检查日志中是否出现密码或完整 Token。

    七条提交逐层补齐安全链

    • ead2197:增加账号与 OAuth 模型。
    • e7e1640:加入 JWT、密码哈希与环境配置。
    • ec74992:继续调整认证相关实现,提交说明本身较泛,正文只把它作为演进节点。
    • 666e6c7:加入登录管理器和中间件。
    • dc44ef0:补齐账号设置、密码登录和第三方 OAuth 处理器。
    • f352f3b:修复 Token 生成返回值,提醒基础路径必须测试。
    • d6191f1:把身份与资源校验扩展到工具、数据集、文档和片段服务。

    从账号表到资源级授权,这才是一条完整链。少任何一层,攻击者都可能从另一入口绕过看似完备的登录页面。

    本文目录
    本文目录