首页 / 资讯中心 / 文章详情

lakeFS 认证缓存与密码重置:从“被动等待过期“到“主动绕过缓存“的设计演进

lakeFS 认证缓存与密码重置:从“被动等待过期“到“主动绕过缓存“的设计演进 ★ FEATURED ARTICLE
数据工程数据湖大数据对象存储后端【免费下载链接】lakeFSlakeFS - Data version control for your data lake | Git for data项目地址https://gitcode.com/gh_mirrors/la/lakeFS点击查看免费下载导读本文聚焦 lakeFS 认证体系中的一个经典设计问题用户登录凭据被缓存在认证缓存auth-cache中重置密码后旧凭据仍会在一段时间内存活导致新密码无法立即生效。围绕 evict-user-from-auth-cache-on-reset-password.md 这份设计文档我们将拆解问题的根因、两种候选解决方案的取舍并结合仓库源码验证最终落地的架构决策。读完本文你将掌握 lakeFS 认证缓存的工作机制、auth.cache.*配置项的实际作用以及避免分布式缓存一致性难题的设计思路。一、问题背景登录凭据为何会被缓存1.1 认证缓存存在的意义lakeFS 在用户登录后会把用户的凭据写入认证缓存其核心动机在 cache.go 的实现中体现得很清楚When a user logs in to lakeFS we save their credentials in the auth-cache. This way, we avoid checking the user credentials through the DB on each call the user make.即每次请求都直接查数据库校验凭据成本太高缓存可以大幅降低对底层 KV 存储如 PostgreSQL、DynamoDB 等的访问压力。从 cache.go 的源码可以看到lakeFS 的认证缓存实际由三块独立 LRU 缓存组成type LRUCache struct { credentialsCache cache.Cache // 按 Access Key ID 缓存凭据 userCache cache.Cache // 按 用户名/邮箱 缓存用户对象 policyCache cache.Cache // 按 UserID 缓存策略列表 }GetCredential(accessKeyID, setFn)以 Access Key ID 为键缓存model.CredentialGetUser(UserKey{Username, Email}, setFn)以用户名邮箱为键缓存model.UserGetUserPolicies(userID, setFn)以用户 ID 为键缓存策略集合。三个缓存都通过cache.NewCache(size, expiry, jitterFn)构造支持容量上限、TTL 过期以及 Jitter抖动目的是错开缓存失效时间避免大量 key 同时过期引发 DB 请求尖峰。1.2 问题复现重置密码后旧密码仍在生效文档描述的现象非常具体Currently, when users try to reset their password, their old password is still stored in the auth-cache, so they have to wait till its evicted before they log in using the new password.也就是说当用户重置密码后认证缓存中仍然存着旧的凭据记录。只要缓存条目尚未被淘汰evict用户用新密码登录反而失败、用旧密码却依然可以登录直到缓存条目自然过期或被容量淘汰。从 model.go 看密码的更新与校验路径如下// 更新密码bcrypt 哈希后写入 EncryptedPassword func (u *User) UpdatePassword(password string) error { pw, err : HashPassword(password) if err ! nil { return err } u.EncryptedPassword pw return nil } // 校验密码与存储的 bcrypt 哈希比对 func (u *User) Authenticate(password string) error { return bcrypt.CompareHashAndPassword(u.EncryptedPassword, []byte(password)) }密码本身在数据库中是以bcrypt 哈希形式存储的HashPassword使用bcrypt.DefaultCost这保证了静态凭据的安全但动态流程上的问题是缓存里缓存的用户凭据对象仍然引用旧哈希数据库中的新哈希要等缓存条目过期后才会被重新加载。而默认的缓存 TTL 是 20 秒见下文配置说明加上 Jitter 后实际失效时间在 20~23 秒之间——虽然不长但对刚重置完密码就要立刻用新密码登录的用户来说这一小段窗口期的体验和安全语义都是不可接受的。二、方案一直觉方案重置密码时主动失效缓存2.1 直觉方案的内容文档给出的第一个、也是最直观的思路是The intuitive solution is just to evict the users from the auth cache when resetting their password.即在密码重置的动作里主动把该用户从认证缓存中移除这样下一次登录会强制回源 DB 读取最新凭据。2.2 为什么这个方案行不通分布式缓存的一致性难题文档紧接着点出了该方案的核心障碍The problem with this solution is that we might not necessarily have a centralized cache, so we might need to evict the user from every auth cache he exists in.关键限制在于lakeFS 的认证缓存并非集中式单实例缓存。在实际部署中lakeFS 服务端可以多实例水平扩展每个实例各自持有独立的进程内 LRU 缓存参见 cache.go 中NewLRUCache的实现缓存实例由各服务的BasicAuthService独立创建若采用主动失效方案密码重置请求可能落在实例 A而用户下一次登录却路由到实例 B——必须保证所有实例上的缓存条目都被同时失效否则 B 上的旧缓存仍会让旧密码生效这意味着需要引入跨实例的缓存失效广播/通知机制例如发布订阅、消息队列或共享失效标记复杂度与运维成本会显著上升而且失效通知本身也存在时序、重复与丢失的可靠性问题。因此从多实例、无中心化缓存的现实架构出发主动失效方案需要解决在每一个缓存实例中都精确移除该用户的难题这与该方案直觉、简单的外表形成了反差。三、方案二最终方案邮箱-密码认证绕过认证缓存3.1 方案要点文档给出的最终解法干脆利落This is why the simpler solution here might be just to not use auth cache for email-password authentication. This way, we still wont need to authenticate when users use API calls that use access-key and secret for authentication, and we wont need to do the effort of evicting the user auth from all the auth cache instances.其核心权衡可以概括为三点邮箱-密码登录这类低频、高敏感认证路径不走缓存直接回源 DB 校验。密码重置是低频事件每次登录直查 DB 的开销完全可以接受换来的是重置后立即生效的强一致语义Access Key / Secret 这类 API 调用认证路径继续使用缓存。这类请求频次极高每次 API 调用都要鉴权保持缓存以保障吞吐与低延迟彻底回避跨实例失效的工程复杂度。既然密码类认证不缓存就根本不存在缓存里残留旧密码的问题也就不需要设计跨实例的失效机制。换言之这不是怎么把脏缓存清掉的问题而是让敏感路径从一开始就不进入缓存的结构性解法——把问题从缓存一致性降维成缓存使用边界。3.2 认证路径的源码印证为了理解绕过缓存具体发生在哪条链路上我们需要先看清 lakeFS 的登录接口与认证器实现。1登录 API 定义api/swagger.yml 定义了POST /auth/login请求体为LoginInformationLoginInformation: type: object required: - access_key_id - secret_access_key properties: access_key_id: type: string secret_access_key: type: string值得注意的是接口的security: []表明登录本身是免认证的公开端点用于换取会话 Token。而字段命名access_key_id/secret_access_key恰好印证了文档的核心观点——在 lakeFS 的当前认证模型里邮箱-密码登录与Access Key 认证共享同一套凭据机制。2登录处理器controller.go 中的Login处理器func (c *Controller) Login(w http.ResponseWriter, r *http.Request, body apigen.LoginJSONRequestBody) { ctx : r.Context() user, err : auth.UserByAuth(ctx, c.Authenticator, c.Auth, body.AccessKeyId, body.SecretAccessKey) ... loginTime : time.Now() duration : c.Config.AuthConfig().GetBaseAuthConfig().LoginDuration expires : loginTime.Add(duration) secret : c.Auth.SecretStore().SharedSecret() tokenString, err : auth.GenerateJWTLogin(secret, user.Username, loginTime, expires) ... }登录成功后通过auth.GenerateJWTLogin签发 JWT有效期默认auth.login_duration 7*24h见 defaults.go。3内置认证器authenticator.go 中BuiltinAuthenticator.AuthenticateUser展示了密码即 Secret的语义func (ba *BuiltinAuthenticator) AuthenticateUser(ctx context.Context, username, password string) (string, error) { // Look user up in DB. username is really the access key ID. cred, err : ba.creds.GetCredentials(ctx, username) if err ! nil { return InvalidUserID, err } if subtle.ConstantTimeCompare([]byte(password), []byte(cred.SecretAccessKey)) ! 1 { return InvalidUserID, ErrInvalidSecretAccessKey } return cred.Username, nil }这里有一个细节非常关键当前BuiltinAuthenticator实际上是把用户名当作 Access Key ID、密码当作 Secret Access Key 来做比对注释直接写明 username is really the access key ID并通过subtle.ConstantTimeCompare常量时间比较来防时序攻击。4缓存访问入口user_auth.go 的UserByAuthfunc UserByAuth(ctx context.Context, authenticator Authenticator, authService Service, accessKey string, secretKey string) (*model.User, error) { username, err : authenticator.AuthenticateUser(ctx, accessKey, secretKey) ... user, err : authService.GetUser(ctx, username) ... }而authService.GetUser内部正是通过Cache.GetUser(...)走缓存BasicAuthService.GetUser→getUser→s.cache.GetUser(...)见 basic_service.go。从源码结构看可以合理推断设计文档所描述的邮箱-密码认证绕过缓存落地时主要是针对缓存键的设计边界做文章——即缓存的使用以Access Key 维度的凭据校验为主密码重置这类按用户维度邮箱的高敏感操作不被userCache的陈旧条目所阻碍。这与文档强调的不依赖缓存做密码认证的意图一致。3.3 落地效果CHANGELOG 的实证仓库 CHANGELOG.md 在 v0.66.0 版本2022-05-26的 Bug fixes 中明确记录了该设计的落地Fix: Evict user from authorization cache to reset password (#3407)这说明设计文档描述的方案最终进入了正式发布版本。结合文档结论以绕开缓存取代跨实例失效的取向可以理解为通过调整密码重置相关路径对缓存的使用从根本上消除了旧密码残留在缓存中的问题。四、认证缓存的配置参数与调优参考4.1 配置项一览认证缓存是 lakeFS 服务端auth.cache.*配置组的一部分。默认值定义在 defaults.goauth: cache: enabled: true # 是否启用认证缓存 size: 1024 # 缓存容量上限条目数 ttl: 20s # 缓存条目存活时间TTL jitter: 3s # 缓存过期时间抖动对应的 Go 结构体定义在 params/auth.gotype ServiceCache struct { Enabled bool Size int TTL time.Duration Jitter time.Duration }4.2 各参数的实际影响enabled是否启用缓存。在 basic_service.go 的NewBasicAuthService中enabledfalse时会退化为DummyCache{}if cacheConf.Enabled { cache NewLRUCache(cacheConf.Size, cacheConf.TTL, cacheConf.Jitter) } else { cache DummyCache{} }DummyCache见 cache.go的实现是不缓存、每次调用都直接执行setFn回源这与本文第三部分密码认证绕过缓存的哲学一致——把缓存关掉等于强制每次回源保证强一致但会增加 DB 压力。在生产环境中应保持enabledtrue让高频率的 API 鉴权路径享受缓存红利。sizeLRU 容量上限默认 1024。达到上限后按 LRU 淘汰最久未使用的条目。容量过小会频繁驱逐热门凭据导致回源率上升过大则占用更多内存。ttl条目最长存活时间默认 20 秒。这正是文档描述旧密码残留窗口期的直接来源——在旧架构下重置密码后最多要等ttl jitter才能让旧条目自然失效。TTL 越小凭据变更如重置密码、吊销 Access Key生效越快但缓存命中率越低。jitter过期抖动默认 3 秒。实际过期时间在[ttl, ttljitter]之间随机化避免大量 key 同时过期造成对 DB 的请求尖峰thundering herd。它由cache.NewJitterFn(jitter)引入cache.go。4.3 调优建议结合架构语义高频 API 负载场景可适当增大size与ttl换取更高缓存命中率前提是你能容忍凭据吊销/密码重置的延迟生效安全敏感场景可调小ttl甚至按需enabledfalse换取凭据变更的即时生效代价是每次鉴权都回源 KV 存储无论怎么调按设计文档的结论密码重置语义不应依赖缓存过期——这正是该设计把邮箱-密码认证移出缓存的原因。配置层面ttl/jitter只影响 API 鉴权路径的缓存行为密码重置的即时生效由架构层面保证。五、设计权衡总结与启示5.1 两种方案对比维度方案一重置时主动失效缓存方案二密码认证绕过缓存最终采用核心思路在重置动作中 evict 缓存条目让密码类认证路径根本不进缓存需要的能力跨所有实例精确失效无多实例一致性负担高需广播/通知机制无从源头消除高频 API 鉴权性能保留缓存收益保留缓存收益密码重置生效时效依赖失效机制正确性结构上即时生效实现复杂度中高低5.2 核心启示缓存一致性问题的降维打击当清除脏数据的成本分布式广播、时序、可靠性高于不使用缓存的成本时后者往往是更优解。对于低频、高敏感的操作密码认证牺牲一点缓存收益换取强一致语义是划算的对于高频、低敏感的操作API 鉴权缓存收益则值得保留。缓存与安全语义要分层设计缓存是性能优化手段不应成为安全语义如密码重置后旧凭据立即失效的实现依赖。把敏感路径排除在缓存之外比在缓存上叠加失效机制更稳健。多实例架构下的缓存设计只要认证缓存是每实例一份的进程内缓存无中心化任何写操作后需全局失效的需求都会引入分布式一致性问题。设计时应优先评估能否不缓存该数据。六、延伸阅读设计文档原文evict-user-from-auth-cache-on-reset-password.md缓存实现pkg/auth/cache.goLRUCache/DummyCache缓存装配与降级逻辑pkg/auth/basic_service.go认证器实现pkg/auth/authenticator.goBuiltinAuthenticator登录 API 定义api/swagger.yml密码哈希与校验pkg/auth/model/model.go配置默认值pkg/config/defaults.go版本落地记录CHANGELOG.md赞分享数据工程数据湖大数据对象存储后端【免费下载链接】lakeFSlakeFS - Data version control for your data lake | Git for data项目地址https://gitcode.com/gh_mirrors/la/lakeFS点击查看免费下载相关推荐如何利用 Laravel Tinker 高效管理缓存过期策略从入门到实战如何利用 Laravel Tinker 高效管理缓存过期策略从入门到实战 Laravel Tinker 是 Laravel 框架中强大的 REPL读取 求值开发工具CLIDeepSearcher缓存策略详解多级缓存与过期机制设计DeepSearcher缓存策略详解多级缓存与过期机制设计 DeepSearcher作为开源深度研究工具其高效的缓存策略设计是其能够在私有数据上进行快速推理人工智能大模型RAGAI Agent深度研究知识库OpenCloud 中的 ttlcache v2 演进史从过期回调到单飞加载的 TTL 缓存设计实践OpenCloud 中的 ttlcache v2 演进史从过期回调到单飞加载的 TTL 缓存设计实践 导读 TTLCache 是一个 Go 语言实现的内存键值后端微服务存储认证鉴权创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
阅读完成 · 觉得有帮助?
咨询建站