动态认证方案
本页介绍 wicket 的动态认证方案子系统:它想解决“不重启服务就增删登录方式”的问题。先说清现状,再介绍各组成部分,便于你判断现在能用到什么程度。
现状(v0.1.4)
当前不能通过容器端到端启用
以下两点均已在 v0.1.4 上实测:
- 开关无法生效。
ProviderStore与Resolver是在di.NewRoot()内部创建的单例,创建时读取的DynamicProviders.Enabled是默认值false。宿主拿到*options.WicketOptions时它们已经建好,事后把Enabled改成true,root.Ptr_Resolver().Enabled()仍为false,Register返回dynamic provider subsystem is disabled。StorageProvider、CacheTTL、Clock同理。 - 没有 handler 解读记录。即使方案出现在方案表里,handler 也是按方案名从容器中查找的,而容器只为
wicket、wicket.external、Google、Facebook、Bearer这五个名字注册了 handler。名为acme-sso的方案会得到no authentication handler registered for scheme "acme-sso"(实测:把该名字静态注册后调用Authenticate即得此错误)。Provider.Config在包内被当作不透明数据,目前没有任何 handler 读取它。
因此,v0.1.4 中可直接使用的是 dynamicscheme 包本身:一份带 TTL 缓存的方案记录存储与解析层。你可以在自己的管理后台用它保存“启用了哪些登录方式”,再由宿主代码决定如何呈现与装配。需要运行期新增一个真正可登录的方案时,目前请走 外部登录 中的静态装配方式并重启。
Provider 记录
go
type Provider struct {
Scheme string // 唯一键,对应认证方案名
DisplayName string // 展示名
Enabled bool // 单条开关;为 false 的记录对解析器不可见
Protocol string // 协议标识,转换为方案时成为 HandlerType
Config map[string]string // 协议参数(authority、client_id 等),包内不解读
}包内所有进出存储的记录都会深拷贝(包括 Config map),调用方修改拿到的记录不会影响已存储的数据。
ProviderStore 接口
go
type ProviderStore interface {
GetProvider(scheme string) (*Provider, error)
AllProviders() ([]*Provider, error)
PutProvider(provider *Provider) error
DeleteProvider(scheme string) error
}契约语义:
| 方法 | 语义 |
|---|---|
GetProvider | 不存在时返回 (nil, nil),不是错误 |
AllProviders | 返回全部记录,包括 Enabled == false 的 |
PutProvider | 按 Scheme 插入或整条替换 |
DeleteProvider | 不存在时什么也不做,返回 nil |
接口方法不带 context.Context,也没有租户维度——它是服务器全局的一张表。若用数据库实现,超时需要在实现内部自行控制。
内置实现 dynamicscheme.NewMemoryStore():读写锁保护,AllProviders 按方案名排序,PutProvider 拒绝 nil 和空方案名。
Resolver:带 TTL 缓存的解析层
go
func NewResolver(store ProviderStore, clock Clock, enabled bool, cacheTTL time.Duration) *Resolver| 方法 | 行为 |
|---|---|
Enabled() | 是否启用 |
Get(scheme) | 返回启用中的记录;关闭、不存在、单条被禁用三种情况都返回 (nil, false, nil) |
All() | 所有启用中的记录,按方案名排序;关闭时返回 (nil, nil) |
Register(p) | 写入存储并立即使缓存失效;关闭时报错 |
Remove(scheme) | 从存储删除并立即使缓存失效;关闭时报错 |
缓存规则:
- 缓存的是“启用中记录”的不可变快照,过期时间是
clock.Now() + cacheTTL的绝对时刻,过期后的下一次读取从存储重新加载。 cacheTTL <= 0、或clock为nil时不缓存,每次读取都访问存储。store为nil时退回内存存储。- 通过
Register/Remove做的修改马上可见;绕过 Resolver 直接写存储(例如另一个进程写了数据库)则要等 TTL 过期才可见。多实例部署时,TTL 就是实例间的最大不一致窗口。 - 重新加载时会过滤掉
nil、空方案名和禁用的记录,并按方案名去重排序,即便存储实现返回了重复或无序的数据。
Clock 只要求 Now() time.Time,与 services.SystemClock 兼容;包内从不直接读 time.Now()。
独立使用示例
以下代码已编译并运行(输出见注释):
go
import (
"fmt"
"time"
"github.com/gonewx/wicket/dynamicscheme"
"github.com/gonewx/wicket/services/impl"
)
func main() {
store := dynamicscheme.NewMemoryStore() // 生产中换成你自己的持久化实现
res := dynamicscheme.NewResolver(store, impl.NewSystemClock(), true, 30*time.Second)
_ = res.Register(&dynamicscheme.Provider{
Scheme: "acme-sso", DisplayName: "Acme SSO", Enabled: true, Protocol: "oidc",
Config: map[string]string{"authority": "https://sso.acme.example", "client_id": "wicket"},
})
_ = res.Register(&dynamicscheme.Provider{Scheme: "legacy", Enabled: false, Protocol: "oidc"})
all, _ := res.All()
for _, p := range all {
fmt.Println(p.Scheme, p.DisplayName, p.Config["authority"]) // acme-sso Acme SSO https://sso.acme.example
}
_, ok, _ := res.Get("legacy")
fmt.Println(ok) // false:单条禁用的记录不可见
}与认证管线的关系(设计意图)
容器中的 auth.SchemeProvider 是一个两态包装:
- 子系统关闭时,完全透传静态方案表;
- 子系统开启时,把静态方案与 Resolver 中启用的记录合并:同名时静态方案优先;动态记录转换为
HandlesRequests = true的方案(Protocol作为HandlerType),以便中间件在回调路径上调用它;默认方案的选择始终只看静态配置;Resolver 出错时记一条 warn 日志并降级为纯静态视图,不让请求失败。
这部分逻辑已经就位,缺的是上文所述的“启用时机”与“按协议构造 handler”两块。