客户端与资源
本页说明如何定义“谁可以申请令牌”(Client)和“令牌可以访问什么”(身份资源、API 资源、API 作用域),以及如何把它们交给 wicket。模型都在 github.com/gonewx/wicket/storage/models 包。
一个完整的例子
import (
wicket "github.com/gonewx/wicket"
"github.com/gonewx/wicket/storage/models"
"github.com/gonewx/wicket/utils"
)
// API 作用域:客户端可申请的权限
read := models.NewApiScope()
read.Name = "orders.read"
// API 资源:受保护的 API,归组作用域,并持有内省密钥
apiSecret := models.NewSecret()
apiSecret.Value = utils.Sha256("orders-api-introspection-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")
worker := models.NewClient()
worker.ClientId = "billing-worker"
worker.ClientSecrets = []*models.Secret{secret}
worker.AllowedScopes = []string{"orders.read"}
if err := worker.SetAllowedGrantTypes(wicket.GrantTypesClientCredentials); err != nil {
log.Fatal(err)
}
cfg := root.ServerConfig()
cfg.SetApiScopes([]*models.ApiScope{read})
cfg.SetApiResources([]*models.ApiResource{orders})
cfg.SetClients([]*models.Client{worker})用该客户端申请的访问令牌载荷形如(v0.1.4 实测):
{"aud":["orders"],"client_id":"billing-worker","iss":"http://localhost:5000","scope":["orders.read"], "exp":..., "iat":..., "nbf":...}Client
务必用 models.NewClient() 构造,它会填入下表中的默认值;直接写 &models.Client{} 会得到全零值(例如 Enabled = false、令牌寿命为 0)。
身份与密钥
| 字段 | 默认值 | 说明 |
|---|---|---|
ClientId | — | 必填,且在列表内唯一 |
Enabled | true | |
ClientSecrets | 空 | 见下方“密钥哈希” |
RequireClientSecret | true | 公共客户端(SPA、移动 App)设为 false |
ClientName、Description、ClientUri、LogoUri | 空 | 展示信息,供同意页使用 |
密钥哈希
Secret.Value 存放的是哈希值,不是明文:
s := models.NewSecret() // Type 默认为共享密钥
s.Value = utils.Sha256("change-me") // SHA-256 后再 Base64(标准编码)
// s.Expiration 零值表示永不过期;v0.1.4 中请保持零值(见“客户端凭据”页的警告)校验时 wicket 对客户端提交的明文同时计算 SHA-256 与 SHA-512,按存储值解码后的长度(32 或 64 字节)选择比较,因此 utils.Sha512 也可用。存储值不是合法 Base64 或长度不对时,校验直接失败——不要把明文放进 Value。
允许的授权类型
授权类型字段未导出,必须通过方法设置:
err := client.SetAllowedGrantTypes(wicket.GrantTypesCode) // 覆盖
err = client.AddAllowedGrantTypes(wicket.GrantTypeClientCredentials) // 追加一个SetAllowedGrantTypes 会拒绝:nil、含空格的值、重复值,以及 implicit 与 authorization_code、implicit 与 hybrid、authorization_code 与 hybrid 的组合。常用预置组合:GrantTypesClientCredentials、GrantTypesCode、GrantTypesCodeAndClientCredentials、GrantTypesDeviceFlow、GrantTypesBackchannelAuthentication、GrantTypesPassword。
授权码流相关
| 字段 | 默认值 | 说明 |
|---|---|---|
RedirectUris | 空 | 回跳地址白名单,授权请求的 redirect_uri 必须精确匹配其一 |
PostLogoutRedirectUris | 空 | 结束会话后允许跳回的地址 |
RequirePkce | true | |
AllowPlainTextPkce | false | 只接受 S256 |
RequireConsent | false | 为 true 时把用户送往 ConsentUrl |
AllowRememberConsent | true | |
ConsentLifetime | nil | v0.1.4 未被读取 |
AllowAccessTokensViaBrowser | false | |
AllowOfflineAccess | false | 为 true 且请求 offline_access 作用域时签发刷新令牌 |
AllowedScopes | 空 | 可申请的作用域名(身份资源名或 API 作用域名) |
令牌寿命(单位:秒)
| 字段 | 默认值 |
|---|---|
AccessTokenLifetime | 3600(1 小时) |
IdentityTokenLifetime | 300 |
AuthorizationCodeLifetime | 300 |
AbsoluteRefreshTokenLifetime | 2592000(30 天),v0.1.4 未被读取 |
SlidingRefreshTokenLifetime | 1296000(15 天),v0.1.4 未被读取 |
DeviceCodeLifetime | 300 |
其他令牌相关字段:
AccessTokenType默认wicket.AccessTokenTypeJwt。设为AccessTokenTypeReference在 v0.1.4 中不生效,仍签发 JWT,见 核心概念。- 以下字段有默认值,但 v0.1.4 的源码中没有读取它们,设置后不会改变行为:
RefreshTokenUsage(默认TokenUsageOneTimeOnly)、RefreshTokenExpiration(默认TokenExpirationAbsolute)、两个刷新令牌寿命、ConsentLifetime、ClientClaimsPrefix(默认"client_")。 IncludeJwtId默认true,但该值没有传递到令牌上,实测签发的访问令牌不含jti。
刷新令牌的实际行为见 刷新令牌。
AllowedCorsOrigins
wicket v0.1.4 不会根据 Client.AllowedCorsOrigins 输出任何 CORS 响应头;它可以作为宿主 CORS 中间件的判定数据(经容器中的 CorsPolicyService),做法见 CORS 跨域。
IdentityResource:身份资源
内置构造器覆盖 OIDC 标准作用域:
| 构造器 | 作用域 | 说明 |
|---|---|---|
models.IdentityResourcesOpenId() | openid | Required = true,声明 sub |
models.IdentityResourcesProfile() | profile | name、family_name 等标准资料声明 |
models.IdentityResourcesEmail() | email | |
models.IdentityResourcesPhone() | phone | |
models.IdentityResourcesAddress() | address |
自定义身份资源:
membership := models.NewIdentityResource()
membership.Name = "membership"
membership.DisplayName = "会员信息"
membership.UserClaims = []string{"member_tier", "member_since"}
cfg.SetIdentityScopes([]*models.IdentityResource{
models.IdentityResourcesOpenId(),
models.IdentityResourcesProfile(),
membership,
})注意注入方法名是 SetIdentityScopes。UserClaims 只声明“这个作用域涵盖哪些声明类型”,声明的具体取值来自宿主通过 cfg.SetProfileProvider 注入的用户资料服务;UserInfo 端点按它过滤返回的声明。
ApiScope 与 ApiResource
ApiScope:一项权限。字段来自内嵌的Resource:Name、DisplayName、Description、Enabled(默认true)、ShowInDiscoveryDocument(默认true,控制是否出现在发现文档的scopes_supported中)、UserClaims,另有Required、Emphasize。v0.1.4 中 API 作用域与 API 资源的UserClaims只用于发现文档的claims_supported列表,不会把用户声明加入访问令牌。ApiResource:一个 API。Scopes列出它包含的 API 作用域名称;ApiSecrets是该 API 调用内省端点时用的密钥(同样以utils.Sha256哈希);AllowedAccessTokenSigningAlgorithms字段存在,但 v0.1.4 的签发路径未读取它。
访问令牌的 aud 来自被请求作用域所属的 API 资源名。只定义 ApiScope 而不定义包含它的 ApiResource 也能签发令牌,但令牌没有资源受众,资源方也无法用自己的身份调用内省端点。
一律用 NewApiScope()、NewApiResource()、NewIdentityResource() 构造——它们会初始化内嵌的 *Resource 指针,否则访问 Name 会空指针 panic。
静态列表与动态 Provider
ServerConfig 上每种配置数据都有两个注入方法:
| 静态 | 动态 |
|---|---|
SetClients([]*Client) | SetClientsProvider(func() []*Client) |
SetIdentityScopes(...) | SetIdentityScopesProvider(func() ...) |
SetApiResources(...) | SetApiResourcesProvider(func() ...) |
SetApiScopes(...) | SetApiScopesProvider(func() ...) |
静态方法只是把切片包装成一个总是返回它的 provider。区别在于 provider 函数会在每次解析客户端存储或资源存储时被调用——在请求处理中频繁发生,一次请求可能调用多次。因此动态 provider 适合“配置可在运行时变化”的场景(例如客户端存在数据库里、后台可编辑),但要满足:
- 快且不阻塞:不要在里面直接查数据库。推荐后台定期刷新、provider 只读一个原子快照。
- 并发安全:会被多个请求 goroutine 同时调用。
- 结果合法:返回的客户端列表中
ClientId为空或重复时,构造客户端存储会直接 panic。
var clients atomic.Pointer[[]*models.Client]
// 宿主后台按需刷新
func reloadClients(list []*models.Client) { clients.Store(&list) }
cfg.SetClientsProvider(func() []*models.Client {
if p := clients.Load(); p != nil {
return *p
}
return nil
})