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:把身份与资源校验扩展到工具、数据集、文档和片段服务。
从账号表到资源级授权,这才是一条完整链。少任何一层,攻击者都可能从另一入口绕过看似完备的登录页面。