客户端与资源
涵盖 Identity Server 的客户端、身份资源、API 作用域、API 资源的配置模型,以及签名凭据注册和配置文件加载。
适用场景
- 需要为应用注册 OAuth 客户端,配置授权类型和回调地址
- 需要定义身份资源,控制令牌中包含哪些用户声明
- 需要定义 API 作用域,控制客户端可申请的权限范围
- 需要配置签名凭据和密钥管理
前置条件
客户端与资源配置依赖 Identity Server 核心注册,详见 快速开始 和 授权类型。
快速开始
Identity Server 用三层模型定义"谁能访问什么":
Client 定义"谁在请求",指定授权类型、可访问的作用域和客户端密钥。ApiResource 定义"保护什么",指定哪些作用域可以访问该 API。中间的 ApiScope 是桥梁——客户端申请作用域,API 资源声明接受哪些作用域。
// 客户端:谁在请求令牌
let clients = [
Client("spa", []).also { c =>
c.requireSecret = false
c.allowedGrantTypes = [GrantTypes.AuthorizationCode]
c.allowedScopes = ["openid", "profile", "api"]
c.allowedRedirectUris = ["http://localhost:3000/callback"]
}
]
// 身份资源:令牌中包含的用户声明
let identityResources = [
IdentityResource.OpenId,
IdentityResource.Profile
]
// API 作用域:客户端可以申请的权限
let apiScopes = [ApiScope("api", Some("API 访问权限"))]
// API 资源:受保护的 API,声明接受哪些作用域
let apiResources = [ApiResource("order-api", ["api"])]
// 签名凭据:用什么密钥签名 JWT
let signingCredentials = [
SigningCredentials(rsaKey, SecurityAlgorithms.RsaSha256)
]核心概念
三层资源模型
Client(客户端) IdentityResource(身份资源) ApiResource(API 资源)
├── clientId ├── name(如 openid) ├── name(如 order-api)
├── allowedGrantTypes ├── claimTypes ├── allowedScopes
├── allowedScopes └── required ├── apiSecrets
├── clientSecrets └── claimTypes
└── allowedRedirectUris
│
ApiScope(作用域)
├── name(如 api)
└── required- Client → 定义谁能请求令牌、用什么方式、能申请哪些作用域
- ApiScope → 定义有哪些权限可以申请
- ApiResource → 定义哪些 API 接受哪些作用域
- IdentityResource → 定义令牌中包含哪些用户身份声明
Client 配置
Client 是客户端的完整配置,关键字段:
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
clientId | String | "" | 客户端唯一标识,必填 |
clientName | String | "" | 显示名称,用于同意页面 |
requireSecret | Bool | true | 是否要求客户端密钥——公开客户端设为 false |
clientSecrets | Array<Secret> | [] | 客户端密钥列表,支持 plain/sha256/sha384/sha512 |
allowedGrantTypes | Array<String> | [] | 允许的授权类型,如 ["authorization_code", "password"] |
allowedScopes | Array<String> | [] | 允许申请的作用域,如 ["openid", "profile", "api"] |
allowedRedirectUris | Array<String> | [] | 允许的回调地址白名单(授权码模式必填) |
postLogoutRedirectUris | Array<String> | [] | 登出后允许跳转的地址白名单 |
requirePkce | Bool | false | 是否强制 PKCE——公开客户端建议 true |
allowPlainTextPkce | Bool | true | 是否允许 plain 挑战方法(建议仅允许 S256) |
requireConsent | Bool | false | 是否需要用户同意授权 |
allowOfflineAccess | Bool | false | 是否允许刷新令牌(离线访问) |
accessTokenLifetime | Int64 | 3600 | 访问令牌有效期(秒),默认 1 小时 |
identityTokenLifetime | Int64 | 300 | 身份令牌有效期(秒),默认 5 分钟 |
authorizationCodeLifetime | Int64 | 300 | 授权码有效期(秒),默认 5 分钟 |
absoluteRefreshTokenLifetime | Int64 | 2592000 | 刷新令牌绝对过期(秒),默认 30 天 |
slidingRefreshTokenLifetime | Int64 | 1296000 | 滑动刷新生命周期(秒),默认 15 天 |
refreshTokenExpiration | RefreshTokenExpiration | Absolute | 过期策略:Absolute 或 Sliding |
refreshTokenUsage | RefreshTokenUsage | ReUse | 使用策略:ReUse 或 Once |
密钥类型——clientSecrets 中的 Secret 的值会根据 type 字段做哈希处理:
// 明文存储
Secret("my-secret")
// SHA256 哈希
Secret("my-secret".sha256())配置文件中通过 type 字段指定:"plain"、"sha256"、"sha384"、"sha512"。ConfigurationLoader 在加载时自动计算哈希。
IdentityResource
IdentityResource 定义令牌中应包含哪些用户声明。框架内置了 4 个标准身份资源:
| 静态常量 | 名称 | 默认声明 |
|---|---|---|
IdentityResource.OpenId | openid | sub |
IdentityResource.Profile | profile | profile(业务自定义) |
IdentityResource.Email | email | email |
IdentityResource.Address | address | address |
自定义身份资源:
// 方式一:直接构造
let customResource = IdentityResource("custom", ["tenant_id", "department"])
// 方式二:从配置文件加载
builder.services.addIdentityServer()
.addInMemoryIdentityResources(builder.configuration.getSection("identityserver:identityResources"))关键字段:
| 字段 | 说明 |
|---|---|
name | 资源名称(也是作用域名) |
claimTypes | 该资源关联的用户声明类型列表 |
required | 是否必须在请求中显式指定——true 时不能省略 |
enabled | 是否启用 |
discoveryDocument | 是否出现在 OIDC 发现文档中 |
ApiScope
ApiScope 定义一个可被客户端申请的权限。一个作用域可以被多个 API 资源引用:
// 单个作用域
ApiScope("read", Some("读取权限"))
// 多个作用域
let scopes = [
ApiScope("read", Some("读取权限")),
ApiScope("write", Some("写入权限")),
ApiScope("admin", Some("管理权限"))
]required 字段——当 required = true 时,客户端必须在请求中显式指定这个作用域,不能被默认授予。这用于防止权限被意外扩大。
ApiResource
ApiResource 定义一个受保护的 API 资源——它是一个或多个作用域的容器:
ApiResource("order-api", ["read", "write", "admin"])关键字段:
| 字段 | 说明 |
|---|---|
name | API 资源名称 |
allowedScopes | 该 API 接受的作用域列表 |
apiSecrets | API 自身的密钥(用于自省端点等场景) |
claimTypes | 访问该 API 时令牌应附带的自定义声明 |
配置文件加载
客户端和资源的定义可从 appsettings.json 加载,避免硬编码大量配置对象:
{
"identityserver": {
"clients": [
{
"clientId": "interactive.public",
"clientName": "交互式公开客户端",
"requireSecret": false,
"requireConsent": true,
"allowedScopes": ["profile", "openid", "api"],
"allowedRedirectUris": ["http://127.0.0.1:3000/callback.html"],
"allowedGrantTypes": ["authorization_code", "refresh_token"],
"accessTokenLifetime": 3600,
"requirePkce": true
}
],
"identityResources": [
{ "name": "openid", "required": true, "claimTypes": ["sub"] },
{ "name": "profile", "claimTypes": ["name", "email", "picture"] }
],
"apiScopes": [
{ "name": "api", "displayName": "API 访问", "description": "允许访问 API" }
],
"apiResources": [
{ "name": "order", "displayName": "Order API", "allowedScopes": ["api"] }
]
}
}代码侧用 IConfiguration 的 getSection 加载:
builder.services.addIdentityServer() { options =>
options.issuer = "spire-idp"
options.issuerUri = "http://localhost:5000"
}
.addInMemoryClients(builder.configuration.getSection("identityserver:clients"))
.addInMemoryApiScopes(builder.configuration.getSection("identityserver:apiScopes"))
.addInMemoryApiResources(builder.configuration.getSection("identityserver:apiResources"))
.addInMemoryIdentityResources(
builder.configuration.getSection("identityserver:identityResources"))ConfigurationLoader 自动将 JSON 字段映射到对应的模型属性,包括枚举值(如 refreshTokenExpiration 的 "Absolute" / "Sliding")和密钥类型的自动哈希。
签名凭据
签名凭据决定 JWT 令牌的签名算法和密钥:
// 对称密钥(仅开发环境)
let symmetricKey = SymmetricSecurityKey("your-256-bit-secret...".toArray())
let hmacCreds = SigningCredentials(symmetricKey, SecurityAlgorithms.HmacSha256)
// RSA 非对称密钥(生产环境推荐)
let rsaKey = RsaSecurityKey.fromPemFiles("public.pem", "private.pem")
let rsaCreds = SigningCredentials(rsaKey, SecurityAlgorithms.RsaSha256)
// ECDSA 非对称密钥(更小的密钥 + 同等安全性)
let ecKey = ECDsaSecurityKey.fromPemFiles("ec_public.pem", "ec_private.pem")
let ecCreds = SigningCredentials(ecKey, SecurityAlgorithms.EcdsaSha256)注册多组凭据后,JWKS 端点(/.well-known/openid-configuration/jwks)会暴露所有公钥,JWT 头部通过 kid 字段标识使用了哪组密钥。
常见用法
公开客户端 vs 机密客户端
// 公开客户端:SPA、移动端——不要求密钥,必须 PKCE
let publicClient = Client("spa-app", []).also { c =>
c.requireSecret = false
c.requirePkce = true
c.allowedGrantTypes = [GrantTypes.AuthorizationCode]
c.allowedRedirectUris = ["http://localhost:3000/callback"]
c.allowedScopes = ["openid", "profile", "api"]
}
// 机密客户端:后端服务——要求密钥,可使用密码模式
let confidentialClient = Client("backend", [Secret("secret".sha256())]).also { c =>
c.requireSecret = true
c.allowedGrantTypes = [GrantTypes.Password, GrantTypes.ClientCredentials]
c.allowedScopes = ["api"]
}存储替换入口
所有内存存储都支持替换。以客户端存储为例:
// 默认:内存存储
builder.services.addIdentityServer()
.addInMemoryClients([...])
// 替换:自定义数据库存储
builder.services.addIdentityServer()
.addClientStore<MyDatabaseClientStore>()其他存储接口(IApiResourceStore、IApiScopeStore、IIdentityResourceStore、ISigningCredentialStore、IConsentStore、IRefreshTokenStore、IAuthorizationCodeStore)都遵循相同的模式——默认用 addInMemoryXxx(),替换用 addXxxStore<T>()。
生产建议
- 公开客户端必须设置
requirePkce = true。 PKCE 是防止授权码拦截攻击的业界标准,OAuth 2.1 将其列为公开客户端的强制要求。如果客户端无法安全保管client_secret(如 SPA、移动端),PKCE 是唯一的安全保障。 - 客户端密钥使用
sha256而非plain。ConfigurationLoader支持自动哈希,配置文件中写"type": "sha256"即可。即使配置文件泄露,攻击者拿到的也只是哈希值而非原始密钥。 allowedRedirectUris必须精确匹配。 不要使用通配符或模糊匹配——回调地址白名单是防止授权码被重定向到恶意地址的最后防线。- 生产环境签名凭据替换为
ISigningCredentialStore实现。 默认的SigningCredentialMemoryStore将密钥存于内存,无法轮换。替换为数据库或密钥管理服务的实现后,可以在不重启服务的情况下轮换密钥。
常见问题
ApiScope 和 ApiResource 为什么分开
ApiScope 是"权限"(如 read、write),ApiResource 是"需要该权限的服务"(如 order-api、user-api)。一个 read 作用域可以被多个 API 资源引用,实现权限的跨 API 复用。客户端申请的是作用域,API 资源声明接受哪些作用域——两者解耦后,可以按需组合而无需修改客户端配置。
offline_access 作用域和 allowOfflineAccess 是什么关系
allowOfflineAccess = true(Client 级别)是前提——开启后该客户端才能请求刷新令牌。offline_access 作用域(ApiScope 级别)是可选的显式声明——如果定义了该作用域,客户端必须在请求中包含它才能获得 refresh_token。
什么情况下需要 claimTypes
在 IdentityResource 和 ApiResource 上都可以指定 claimTypes。IdentityResource 的 claimTypes 控制该身份资源激活时令牌中会包含哪些用户声明。ApiResource 的 claimTypes 控制访问该 API 时令牌应附带的额外声明(如租户 ID、API 版本等)。
相关专题
- 需要了解授权类型 → 授权类型
- 需要了解最小可运行示例 → 快速开始
- 需要了解 Identity Server 整体架构 → Identity Server 总览
- 需要了解 JWT 签名算法 → JWT 令牌