Skip to content

Claim

本章只对应 soulsoft_identity_claims 这个库本身。

它只提供三层身份数据模型:

  • Claim:一条声明
  • ClaimsIdentity:一组声明 + 一个认证类型
  • ClaimsPrincipal:一组身份

核心结构

这套模型最重要的关系是:

text
Claim -> ClaimsIdentity -> ClaimsPrincipal

也就是说:

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

最小示例

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")

这里没有任何“用户对象”或“权限对象”。整个身份信息就是这些声明本身。

Claim

Claim 只有三个字段:

  • type
  • value
  • valueType

构造方式:

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])
    }
}

这里还有一个实现层含义:

  • 这个选择器是 ClaimsPrincipal 的全局静态状态
  • 改掉后会影响后续所有 principal.identity 的行为

角色判断

isInRole(role) 的实现也很简单:

  • 遍历所有 identity
  • 检查是否存在 ClaimTypes.Role
  • 且 value 等于传入的 role

所以它只认:

text
ClaimTypes.Role

不会自动去看 ClaimTypes.Rolesscope 或其它自定义声明。

序列化与克隆

这三个核心类型都实现了序列化:

  • Claim.serialize() / deserialize(...)
  • ClaimsIdentity.serialize() / deserialize(...)
  • ClaimsPrincipal.serialize() / deserialize(...)

同时也都支持深拷贝:

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

clone() 的行为是“复制数据”,不是共享内部集合:

  • ClaimsIdentity.clone() 会逐条复制 claim
  • ClaimsPrincipal.clone() 会逐个调用 identity 的 clone()

ClaimTypes

ClaimTypes 是一组字符串常量,源码里按类别分了几组。

标准令牌常量

cangjie
ClaimTypes.Sub
ClaimTypes.Exp
ClaimTypes.Iss
ClaimTypes.Aud
ClaimTypes.Iat
ClaimTypes.Jti

OpenID Connect

cangjie
ClaimTypes.Name
ClaimTypes.Email
ClaimTypes.EmailVerified
ClaimTypes.PhoneNumber
ClaimTypes.PreferredUsername
ClaimTypes.Picture
ClaimTypes.Sid

角色 / 权限

cangjie
ClaimTypes.Role
ClaimTypes.Roles
ClaimTypes.Scope
ClaimTypes.Permissions

组织或业务标识

cangjie
ClaimTypes.TenantId
ClaimTypes.ClientId
ClaimTypes.UserId
ClaimTypes.EmployeeId

这些常量本质上只是字符串别名,没有额外行为。

ClaimValueTypes

ClaimValueTypes 也是字符串常量,只是用来描述 Claim.valueType

常见值包括:

cangjie
ClaimValueTypes.String
ClaimValueTypes.Boolean
ClaimValueTypes.Integer
ClaimValueTypes.Date
ClaimValueTypes.DateTime
ClaimValueTypes.Email

它们来自几类命名空间:

  • XML Schema
  • SOAP
  • XML Signature
  • XQuery
  • XACML

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

几个实用结论

1. 这个库只负责身份数据模型

它不负责:

  • 登录流程
  • 声明校验
  • 授权决策

2. isAuthenticated 只看 authenticationType

不是看有没有 claim,也不是看 claim 是否完整。

3. 类型和值匹配默认是精确匹配

ClaimsIdentity.findAll(type)hasClaim(type, value) 都没有做大小写归一化。

4. isInRole 只检查 ClaimTypes.Role

如果你把角色放在 "roles""permissions"isInRole 不会自动识别。

5. principal.identity 受全局静态选择器影响

如果你调用了 ClaimsPrincipal.setPrimaryIdentitySelector(...),就相当于改了整个进程里主身份的选取规则。