Skip to content

动态认证方案 ​

本页介绍 wicket 的动态认证方案子系统:它想解决“不重启服务就增删登录方式”的问题。先说清现状,再介绍各组成部分,便于你判断现在能用到什么程度。

现状(v0.1.4) ​

当前不能通过容器端到端启用

以下两点均已在 v0.1.4 上实测:

  1. 开关无法生效。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 同理。
  2. 没有 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”两块。

相关阅读 ​

以 Apache License 2.0 发布 · 文档内容 © 2026 Wicket 作者