嵌入宿主应用
本页说明 wicket 在宿主进程里是怎样一层层包起来的、哪些请求它处理、哪些交还给你,以及启动和关闭时宿主要做什么。
装配骨架
root, err := di.NewRoot()
if err != nil {
log.Fatal(err)
}
defer root.Close()
opts := godi.GetRequired[*options.WicketOptions](root)
opts.IssuerUri = "https://id.example.com"
cfg := root.ServerConfig()
// ... cfg.SetClients / SetApiScopes / SetSigningCredentialStores / 各类 store ...
cfg.Freeze() // 校验存储注入;失败直接 panic
srv, err := server.DefaultServer(root, server.WithLogger(logger))
if err != nil {
log.Fatal(err)
}
app := http.NewServeMux() // 你的业务路由
handler := hosting.ScopeMiddleware(root)(srv.UseWicket()(app))
log.Fatal(http.ListenAndServe(":5000", handler))可直接运行的完整版本见 快速开始。
中间件链顺序
一个请求从外到内依次经过:
hosting.ScopeMiddleware(root) 为本次请求创建 DI 作用域,放进 context
└─ srv.UseWicket() 返回的中间件
├─ 动态认证方案中间件 处理运行时注册的外部登录方案(未启用时直接放行)
├─ 核心认证中间件 按认证方案识别当前用户
└─ 端点路由 路径命中协议端点 → wicket 处理并结束
未命中 → 交给 next(你的 app)两条硬性要求:
ScopeMiddleware必须在最外层。 端点路由从 context 取作用域,取不到时不会报错,而是把所有请求原样透传给宿主——表现为“协议端点全部 404/落到你的业务路由”。漏包这一层是最常见的装配错误。UseWicket()只调用一次,在装配完成之后。它会在调用当下执行启动校验(见下文),并解析路由器等单例。
哪些路径由 wicket 处理
端点路由对 r.URL.Path 做不区分大小写的精确匹配,命中以下路径之一才处理:
| 路径 | 端点 |
|---|---|
/connect/authorize、/connect/authorize/callback | 授权 |
/connect/token | 令牌 |
/.well-known/openid-configuration | 发现文档 |
/.well-known/openid-configuration/jwks | JWKS |
/connect/userinfo | UserInfo |
/connect/introspect | 内省 |
/connect/endsession、/connect/endsession/callback | 结束会话 |
/connect/checksession | 会话检查 iframe |
/connect/ciba | CIBA 后端通道认证 |
其余所有路径都交给宿主 handler,包括 /connect/revocation、/connect/deviceauthorization 和 mTLS 路径——它们在 v0.1.4 只有常量、没有处理器。你的登录页、同意页、业务 API 也就是这样和 wicket 共存的。
只能挂在根路径
wicket 生成的 URL(发现文档里的端点、登录重定向里的回调地址)都以站点根为基准,BasePath 恒为空。不要用 http.StripPrefix 把它挂到子路径下。
Freeze 与启动期失败
wicket 的启动校验都以 panic 的形式出现,不返回 error。建议在进程启动阶段完成全部装配,让问题在部署时就暴露。
| 时机 | 校验内容 | 失败表现 |
|---|---|---|
cfg.Freeze() | 11 个存储 seam 是否都已注入;或宿主已调用 cfg.AllowInMemoryFallback() | panic:store seam "..." not injected by host and in-memory fallback not declared |
srv.UseWicket() | ErrorUrl、ConsentUrl、ConsentReturnUrlParameter、LogoutIdParameter、ErrorIdParameter、CustomRedirectReturnUrlParameter、CheckSessionCookieName、CorsPolicyName 不能为空 | panic:如 ConsentUrl is not configured |
| 首次使用某个 store | 若未调用 Freeze 且未注入:回退内存实现并输出一条 warn 日志 | 静默运行在内存里 |
关于 Freeze 需要知道的事实:
- 它只做一次注入校验并置位标记,不会让
ServerConfig变成只读——之后的SetXxx调用仍会生效。请把它当作“装配完成”的检查点,而不是并发保护。 - 不调用
Freeze也能运行,但未注入的 store 会悄悄回退到进程内存,多副本部署时必然出错。生产装配务必调用。 AllowInMemoryFallback()必须在Freeze()之前调用才有意义,且只用于开发环境,见 持久化存储。
生命周期与 Root.Close
root, err := di.NewRoot()
// ...
defer root.Close()Root.Close()按构造的逆序释放容器跟踪的 store 包装器:如果宿主注入的适配器实现了io.Closer,会转发调用其Close;没实现的什么也不做。Close是幂等的,第二次调用返回第一次的结果;关闭之后再从容器解析服务会 panic。- 宿主要先停止接收请求(例如
http.Server.Shutdown),再调用root.Close()。关闭过程中仍有请求在用适配器属于宿主错误,wicket 不做同步。 - 宿主自己创建的资源(数据库连接池、授权参数消息 store 等)由宿主自己关闭。例如 wicket-pg 的 store 不实现
Close,root.Close()不会关闭pgxpool.Pool。
推荐的关闭顺序(示意):
_ = httpServer.Shutdown(ctx) // 1. 停止接收新请求,等待在途请求结束
cancelCleanup() // 2. 停止宿主的过期清理循环
_ = root.Close() // 3. 释放 wicket 容器
pool.Close() // 4. 关闭宿主自己的连接池与路由器集成
UseWicket() 返回的是标准的 func(http.Handler) http.Handler,ScopeMiddleware 也是。因此任何能接受 http.Handler 的路由器都可以集成,原则只有一条:wicket 包在路由器外面,让协议路径在进入路由器之前就被截走。
// 示意:buildMyRouter 返回宿主的路由器
var router http.Handler = buildMyRouter() // 标准库 mux、chi、gin 引擎……任何 http.Handler
handler := hosting.ScopeMiddleware(root)(srv.UseWicket()(router))
http.ListenAndServe(":5000", handler)- 标准库
http.ServeMux、chi 的chi.Mux都直接实现了http.Handler。 - gin 的
*gin.Engine同样实现了http.Handler,可以作为上面的router传入。
不建议把 wicket 注册成路由器里的某条子路由:wicket 需要同时接管 /connect/* 与 /.well-known/* 两组路径,并依赖根路径计算 URL。
登录、同意与错误页面由宿主实现
wicket 不渲染任何交互页面。授权请求需要用户交互时,它会把浏览器 302 到 WicketOptions.UserInteraction 中配置的地址,并附上一个返回地址参数;宿主页面处理完后再把浏览器送回这个返回地址。
NewRoot() 之后各字段的实际值(已用 v0.1.4 实测):
| 字段 | 默认值 | 说明 |
|---|---|---|
LoginUrl | 空 | 必须由宿主设置,见下方警告 |
LoginReturnUrlParameter | 空 | 建议设为 "returnUrl" |
LogoutUrl | 空 | 结束会话时重定向的宿主登出页 |
LogoutIdParameter | "logoutId" | |
ConsentUrl | "/consent" | |
ConsentReturnUrlParameter | "returnUrl" | |
ErrorUrl | "/home/error" | |
ErrorIdParameter | "errorId" | |
CustomRedirectReturnUrlParameter | "returnUrl" | |
DeviceVerificationUrl | "/device" | v0.1.4 无设备授权端点,暂不起作用 |
CookieMessageThreshold | 2 |
LoginUrl 没有默认值
LoginUrl 与 LoginReturnUrlParameter 在 v0.1.4 默认都是空字符串,UseWicket() 也不会校验它们。不设置的话,未登录用户访问授权端点会被重定向到一个形如 /connect/?=... 的无效地址。凡是会用到授权码流的宿主都应显式设置:
ui := opts.UserInteraction
ui.LoginUrl = "/account/login"
ui.LoginReturnUrlParameter = "returnUrl"
ui.LogoutUrl = "/account/logout"设置后,未登录的授权请求会被重定向到 /account/login?returnUrl=%2Fconnect%2Fauthorize%2Fcallback%3F...。
宿主登录页的职责:校验用户凭据 → 通过 wicket 认证方案写入登录会话 → 重定向回 returnUrl(只接受本站相对地址,防止开放重定向)。如 核心概念 所述,wicket 方案的 cookie handler 需要宿主自己提供。完整的交互流程见 授权码 + PKCE。
日志
server.WithLogger(logger) 只作用于 Server 自身(启动信息等)。容器内各服务使用的日志器在 di.NewRoot() 时就已确定:
v0.1.4 的已知限制
cfg.SetLogger(logger) 在 NewRoot() 之后调用,不会改变已构造服务的日志器——实测中容器内部日志始终以 JSON 格式、Debug 级别、附带源码位置写到标准输出。在 v0.1.4 中宿主无法通过 API 调整其级别或去向,请在日志采集侧过滤 Debug 级别。详见 日志与可观测性。