Skip to content

快速开始 ​

本页带你从空目录开始,写一个约 100 行的 Go 程序:它把 wicket 挂到标准库 net/http 上,注册一个后台服务客户端,然后用 curl 拿到一枚 JWT 访问令牌,再通过内省端点确认它有效。

整个示例在 wicket v0.1.4 上逐行跑通过,源码位于仓库的 examples/quickstart/。

准备 ​

需要说明
Go 1.27wicket 的 go.mod 声明 go 1.27,更低版本无法解析依赖
curl用于手动调用端点
bash
mkdir wicket-quickstart && cd wicket-quickstart
go mod init example.com/quickstart
go get github.com/gonewx/wicket@v0.1.4

关于 go.work

如果你的机器上有全局 go.work,拉取依赖时请加 GOWORK=off,避免工作区把 wicket 解析到本地目录。

完整代码 ​

把下面的内容保存为 main.go,然后执行 go mod tidy。

go
package main

import (
	"crypto/rand"
	"crypto/rsa"
	"log"
	"log/slog"
	"net/http"
	"os"

	"github.com/gonewx/godi"
	wicket "github.com/gonewx/wicket"
	"github.com/gonewx/wicket/config/options"
	"github.com/gonewx/wicket/di"
	"github.com/gonewx/wicket/hosting"
	"github.com/gonewx/wicket/server"
	wicketmodels "github.com/gonewx/wicket/models"
	"github.com/gonewx/wicket/storage"
	"github.com/gonewx/wicket/storage/memory"
	"github.com/gonewx/wicket/storage/models"
	"github.com/gonewx/wicket/tokens"
	"github.com/gonewx/wicket/utils"
	"github.com/lestrrat-go/jwx/v3/jwk"
)

func main() {
	logger := slog.New(slog.NewTextHandler(os.Stderr, nil))

	// 1. Composition root: wires every Wicket service
	root, err := di.NewRoot()
	if err != nil {
		log.Fatal(err)
	}
	defer root.Close()

	// 2. Protocol options: the issuer identifier
	opts := godi.GetRequired[*options.WicketOptions](root)
	opts.IssuerUri = "http://localhost:5000"

	// 3. Register resources and clients
	api := models.NewApiScope()
	api.Name = "orders.read"

	apiSecret := models.NewSecret()
	apiSecret.Value = utils.Sha256("api-secret")
	orders := models.NewApiResource()
	orders.Name = "orders"
	orders.Scopes = []string{"orders.read"}
	orders.ApiSecrets = []*models.Secret{apiSecret}

	secret := models.NewSecret()
	secret.Value = utils.Sha256("change-me")

	client := models.NewClient()
	client.ClientId = "billing-worker"
	client.ClientSecrets = []*models.Secret{secret}
	client.AllowedScopes = []string{"orders.read"}
	if err := client.SetAllowedGrantTypes(wicket.GrantTypesClientCredentials); err != nil {
		log.Fatal(err)
	}

	cfg := root.ServerConfig()
	signing, validation := devSigningKey()
	cfg.SetSigningCredentialStores(signing)
	cfg.SetValidationKeysStores(validation)
	cfg.SetApiScopes([]*models.ApiScope{api})
	cfg.SetApiResources([]*models.ApiResource{orders})
	cfg.SetClients([]*models.Client{client})
	cfg.AllowInMemoryFallback() // local development only
	cfg.Freeze()

	// 4. Mount the middleware
	srv, err := server.DefaultServer(root, server.WithLogger(logger))
	if err != nil {
		log.Fatal(err)
	}
	app := http.NewServeMux()
	app.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("host app\n"))
	})
	handler := hosting.ScopeMiddleware(root)(srv.UseWicket()(app))

	log.Println("listening on :5000")
	log.Fatal(http.ListenAndServe(":5000", handler))
}

// devSigningKey creates an in-process RSA signing key (development only: a restart means a new key).
func devSigningKey() ([]storage.SigningCredentialStore, []storage.ValidationKeysStore) {
	raw, err := rsa.GenerateKey(rand.Reader, 2048)
	if err != nil {
		log.Fatal(err)
	}
	// Use the RFC 7638 thumbprint as kid so it matches the kid published in the JWKS
	key, err := jwk.Import(raw)
	if err != nil {
		log.Fatal(err)
	}
	if err := jwk.AssignKeyID(key); err != nil {
		log.Fatal(err)
	}
	kid, _ := key.KeyID()

	cred, err := tokens.NewSigningCredentials(raw, tokens.WithAlgorithm("RS256"), tokens.WithKid(kid))
	if err != nil {
		log.Fatal(err)
	}
	info := &wicketmodels.SecurityKeyInfo{Key: raw.Public(), SigningAlgorithm: cred.Algorithm}
	return []storage.SigningCredentialStore{memory.NewMemorySigningCredentialStore(cred)},
		[]storage.ValidationKeysStore{memory.NewMemoryValidationKeysStore([]*wicketmodels.SecurityKeyInfo{info})}
}

逐段说明 ​

1. 组合根 ​

go
root, err := di.NewRoot()

di.NewRoot() 构造 wicket 的组合根:所有端点处理器、验证器、令牌服务都在这里装配好。它返回 (*Root, error),用完后调用 root.Close() 释放持有的资源(例如注入的数据库连接所对应的 store)。

2. 协议选项 ​

go
opts := godi.GetRequired[*options.WicketOptions](root)
opts.IssuerUri = "http://localhost:5000"

WicketOptions 是协议层的主配置,需在 NewRoot() 之后、UseWicket() 之前修改。IssuerUri 会写进每一枚令牌的 iss 声明,也是发现文档里所有端点 URL 的前缀;生产环境必须是对外可访问的 HTTPS 地址。

自动密钥管理默认关闭,所以示例手动提供了一把进程内的 RSA 签名密钥(见 devSigningKey),通过 ServerConfig 注入。这样最容易理解,但重启即换钥,之前签发的令牌全部无法验签。生产环境的做法见签名密钥。

3. 资源与客户端 ​

示例注册了三样东西:

  • API 作用域 orders.read:客户端可以申请的权限单位。
  • API 资源 orders:一个受保护的后端服务,它拥有 orders.read 作用域。令牌的 aud 声明会写成 orders;资源自己的密钥 api-secret 用于调用内省端点。
  • 客户端 billing-worker:只允许 client_credentials 授权,只能申请 orders.read。

客户端密钥以 SHA-256 哈希形式保存(utils.Sha256("change-me")),明文只出现在调用方。

4. ServerConfig 与冻结 ​

go
cfg := root.ServerConfig()
cfg.SetClients(...)
cfg.AllowInMemoryFallback() // 仅限本地开发
cfg.Freeze()

ServerConfig 是宿主向 wicket 注入数据和实现的唯一入口:客户端、资源、签名密钥、各类存储都从这里进。

  • AllowInMemoryFallback() 显式允许未注入的存储回退到内存实现。不调用它时,只要有一个存储未注入,Freeze() 就会 panic——这是有意为之,防止生产环境悄悄跑在内存上。
  • Freeze() 在装配完成时做一次校验(例如上面的存储检查)。它不会锁住配置——之后调用 setter 仍会生效,所以请把所有注入放在 Freeze() 之前。

5. 挂载中间件 ​

go
handler := hosting.ScopeMiddleware(root)(srv.UseWicket()(app))

两层包装缺一不可:

  1. hosting.ScopeMiddleware(root) 为每个请求创建依赖注入作用域;
  2. srv.UseWicket() 拦截协议路径(/connect/*、/.well-known/*),其余请求原样交给你的 app。

运行并验证 ​

bash
go run .

另开一个终端:

发现文档——确认服务已就绪:

bash
curl -s http://localhost:5000/.well-known/openid-configuration

签发令牌——客户端凭据走 HTTP Basic 认证:

bash
curl -s -u billing-worker:change-me \
  -d grant_type=client_credentials \
  -d scope=orders.read \
  http://localhost:5000/connect/token
json
{
  "access_token": "eyJhbGciOiJSUzI1NiIs...",
  "expires_in": 3600,
  "scope": "orders.read",
  "token_type": "Bearer"
}

把 access_token 的中段做 base64url 解码,可以看到它携带的声明:

json
{
  "aud": ["orders"],
  "client_id": "billing-worker",
  "iss": "http://localhost:5000",
  "scope": ["orders.read"],
  "iat": 1794538592,
  "nbf": 1794538592,
  "exp": 1794542192
}

内省令牌——以 API 资源 orders 的身份调用:

bash
curl -s -u orders:api-secret \
  -d token=<上一步的 access_token> \
  http://localhost:5000/connect/introspect
json
{ "active": true, "aud": ["orders"], "client_id": "billing-worker", "scope": "orders.read", "...": "..." }

宿主路由不受影响:

bash
curl -s http://localhost:5000/
# host app

常见问题 ​

现象原因
令牌端点返回 no signing credential available没有可用的签名密钥:要么注入签名凭据,要么正确配置密钥管理
{"error":"invalid_client"}客户端 ID 或密钥不对,或密钥未用 utils.Sha256 哈希保存
{"error":"invalid_scope"}申请的作用域不在客户端的 AllowedScopes 中,或未注册对应的 ApiScope
Freeze() 时 panic:store seam "..." not injected by host and in-memory fallback not declared有存储未注入,且未调用 AllowInMemoryFallback()

下一步 ​

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