Skip to content

身份声明

涵盖 soulsoft_identity_claims 库的三层身份数据模型——Claim(单条声明)、ClaimsIdentity(一组声明 + 认证类型)、ClaimsPrincipal(多身份容器),是认证产出和授权消费的共同数据基础。

适用场景

  • 需要理解认证中间件产出的 context.user 是什么数据结构
  • 需要在签发 JWT 时构建 claims 列表
  • 需要在自定义授权策略中检查声明值
  • 需要手动构建 ClaimsPrincipal 进行单元测试

不适合仅使用认证中间件默认行为的场景——如果只用到 context.user.isInRole()requireRole(),不需要深入了解 ClaimsIdentity 的内部细节。身份声明只提供数据模型,不参与登录流程、声明校验和授权决策。

前置条件

身份声明只依赖一个包:

用途
soulsoft_identity_claims提供 ClaimClaimsIdentityClaimsPrincipalClaimTypesClaimValueTypes

无需额外配置或 DI 注册——所有类型直接 import soulsoft_identity_claims.* 即可使用。

快速开始

下面是一个最小示例:构建一个带认证的身份、添加声明、跨身份查询。

核心两步:ClaimsIdentity(Some("Bearer")) 创建已认证身份并用 addClaim 逐条写入声明;ClaimsPrincipal 容纳多个身份,findFirstValue 跨身份聚合查询。

cangjie
import soulsoft_identity_claims.*

let id = ClaimsIdentity(Some("Bearer"))
id.addClaim(ClaimTypes.Sub, "user-001")
id.addClaim(ClaimTypes.Name, "spire")
id.addClaim(ClaimTypes.Role, "admin")

let principal = ClaimsPrincipal([id])

let userId = principal.findFirstValue(ClaimTypes.Sub) ?? String.empty
let userName = principal.findFirstValue(ClaimTypes.Name) ?? String.empty
let isAdmin = principal.isInRole("admin")

这段代码跑了完整链路:ClaimClaimsIdentity(Bearer 认证)→ ClaimsPrincipal(多身份容器)→ 跨身份查询。findFirstValue 跨所有 identity 聚合查询,isInRole 只在 ClaimTypes.Role(即 "role")中查找。

核心概念

定位与职责

soulsoft_identity_claims 只负责一件事:定义"用户是谁"的数据结构

三层模型的关系是:

text
Claim -> ClaimsIdentity -> ClaimsPrincipal

也就是说:

  • 单条信息放在 Claim
  • 同一个身份下的多条信息放在 ClaimsIdentity
  • 一个请求或用户可以同时拥有多个身份,最终放在 ClaimsPrincipal

认证中间件产出 ClaimsPrincipal 写入 context.user,授权中间件再从中读取 claims 做权限判定。这个库是两者之间唯一的数据约定。

Claim

Claim 只有三个字段:typevaluevalueType

cangjie
let c1 = Claim("role", "admin")
let c2 = Claim("age", "18", ClaimValueTypes.Integer)

如果不传 valueType,默认值就是:

text
ClaimValueTypes.String

它的字符串形式也很直接:

cangjie
println(c1.toString())
// role = admin [http://www.w3.org/2001/XMLSchema#string]

ClaimsIdentity

ClaimsIdentity 表示一个身份。它内部持有一个 ArrayList<Claim> 和一个可选的 authenticationType

最容易忽略的实现细节是:

text
isAuthenticated == authenticationType.isSome()

也就是说,是否"已认证"只看 authenticationType 有没有值,不看声明内容。

例如:

cangjie
let anonymous = ClaimsIdentity()
println(anonymous.isAuthenticated) // false

let bearer = ClaimsIdentity(Some("Bearer"))
println(bearer.isAuthenticated) // true

添加声明有三种重载:

cangjie
let identity = ClaimsIdentity(Some("AuthA"))

identity.addClaim(ClaimTypes.Name, "spire")
identity.addClaim(ClaimTypes.Role, "admin")
identity.addClaims([
    Claim(ClaimTypes.Email, "spire@example.com"),
    Claim("tenant", "default")
])

查询接口分成三类:findAll(全部匹配)、findFirst(首个匹配)、findFirstValue(首个匹配的值),以及两个存在性判断:hasClaim(predicate)hasClaim(type, value)

匹配语义是精确匹配,不做大小写归一化:

cangjie
identity.findAll("role")
identity.hasClaim("role", "admin")

所以 "role""Role" 不是一回事,"admin""Admin" 也不是一回事。

ClaimsPrincipal

ClaimsPrincipal 是多个 ClaimsIdentity 的容器。

cangjie
let bearer = ClaimsIdentity(Some("AuthA"))
bearer.addClaim(ClaimTypes.Sub, "user-001")

let second = ClaimsIdentity(Some("AuthB"))
second.addClaim(ClaimTypes.Role, "admin")

let principal = ClaimsPrincipal([bearer, second])

它的查询会跨所有 identity 聚合:

cangjie
let sub = principal.findFirstValue(ClaimTypes.Sub)
let role = principal.findFirstValue(ClaimTypes.Role)
let allRoles = principal.findAll(ClaimTypes.Role)

主身份: principal.identity 默认返回第一个 identity,由全局静态选择器决定:

cangjie
ClaimsPrincipal.setPrimaryIdentitySelector { identities =>
    identities |> first
}

你可以改掉它:

cangjie
ClaimsPrincipal.setPrimaryIdentitySelector { identities =>
    let items = identities |> collectArray
    if (items.isEmpty()) {
        None
    } else {
        Some(items[items.size - 1])
    }
}

setPrimaryIdentitySelectorClaimsPrincipal全局静态状态——修改后影响进程内所有后续 principal.identity 的行为。

角色判断: isInRole(role) 遍历所有 identity,检查是否存在 ClaimTypes.Role(即 "role")且 value 等于传入参数。它不会自动识别 ClaimTypes.Roles"roles")、scope 或自定义声明类型。

ClaimTypes 与 ClaimValueTypes

ClaimTypes 是一组字符串常量,按用途分几类:

类别常量用途
JWT 标准SubExpIssAudIatJtiNbfJWT 令牌标准声明字段
JWT 头部TypAlgKidEncCtyJWT header 参数
OpenID ConnectNameEmailEmailVerifiedPhoneNumberPreferredUsernamePictureSidBirthdateGenderAddressOIDC 标准声明
角色/权限RoleRolesScopePermissions授权判定
组织/业务TenantIdClientIdUserIdEmployeeId多租户和业务标识

ClaimValueTypes 同样是字符串常量,用于标注 Claim.valueType。常见值包括 StringBooleanIntegerDateDateTimeEmailDnsNameRsa 等,来自 XML Schema、SOAP、XML Signature、XQuery、XACML 等命名空间。

这个库不会根据 valueType 自动做类型转换——它只是把元数据保存下来。

常见用法

多认证方案共存时的身份查询

当 Bearer + Cookie 等多认证方案共存时,ClaimsPrincipal 可能包含多个 identity。查询会自动跨所有 identity 聚合,无需手动遍历:

cangjie
let bearerId = ClaimsIdentity(Some("Bearer"))
bearerId.addClaim(ClaimTypes.Sub, "user-001")
bearerId.addClaim(ClaimTypes.Name, "spire")

let cookieId = ClaimsIdentity(Some("Cookie"))
cookieId.addClaim(ClaimTypes.Role, "admin")
cookieId.addClaim(ClaimTypes.Email, "spire@example.com")

let principal = ClaimsPrincipal([bearerId, cookieId])

// 跨所有 identity 聚合查询
let name = principal.findFirstValue(ClaimTypes.Name)  // Some("spire") — 来自 Bearer
let role = principal.findFirstValue(ClaimTypes.Role)  // Some("admin") — 来自 Cookie
let isAdmin = principal.isInRole("admin")             // true — 跨 identity 检查

序列化与深克隆

三个核心类型都实现了 Serializable 接口,支持 serialize() / deserialize(),可用于跨进程传递或持久化到缓存。

同时也支持 clone() 深拷贝——修改克隆不会影响原对象:

cangjie
let copiedIdentity = identity.clone()
let copiedPrincipal = principal.clone()

clone() 的行为是"复制数据",不是共享内部集合:ClaimsIdentity.clone() 逐条复制 claim,ClaimsPrincipal.clone() 逐个调用 identity 的 clone()。这在需要临时追加声明但不污染原始身份的场合(如审计日志附加临时 claims)很有用。

生产建议

  • isAuthenticated 只看 authenticationType 是否 Some,不看声明内容。 即使 ClaimsIdentity 中有多条 claim,只要 authenticationTypeNoneisAuthenticated 就是 false。框架用 authenticationType 区分"匿名身份"和"已认证身份",而非用声明数量判断。
  • isInRole 只检查 ClaimTypes.Role(即 "role")。 如果把角色放在 "roles""permissions" 或自定义声明类型中,isInRole 不会自动识别。需要时改用 principal.findFirstValue("your-role-type") 手动检查,或使用自定义授权要求。
  • findAllhasClaim 使用 == 精确匹配,不做大小写归一化。 "role""Role" 被视为不同的声明类型。如果声明来源多样(如不同 OIDC 提供方),建议在写入或查询时统一使用 ClaimTypes 常量来避免大小写不一致。
  • setPrimaryIdentitySelector 是全局静态状态。 修改后影响整个进程,不是线程隔离的。如果只在特定场景需要不同的主身份选取规则,直接在调用处按需获取特定 identity(通过 principal.identities 遍历),不要随意修改全局选择器。

常见问题

isAuthenticatedfalse,但 identity 里有 claims

isAuthenticated 只看 authenticationType 是否 Some。默认构造的 ClaimsIdentity()authenticationTypeNone,所以即使调用了 addClaimisAuthenticated 仍然是 false。要用 ClaimsIdentity(Some("Bearer")) 创建已认证身份。

isInRole("admin") 返回 false,但确实有 admin 声明

先检查声明类型是否是 ClaimTypes.Role(即 "role")而不是 "roles""Role"(大写)或自定义类型。其次检查值的大小写——"admin""Admin" 不匹配。建议始终使用 ClaimTypes.Role 常量和统一大小写的值来避免这类问题。

findAll("Role") 查不到声明

类型匹配是精确区分大小写的。声明写入时用的是 "role"(小写),查询时用 "Role"(首字母大写)不会命中。始终使用 ClaimTypes.Role 之类的常量来保证一致性。

相关专题