- 后端
- 认证鉴权
- 安全
【免费下载链接】kanidm
Kanidm: A simple, secure, and fast identity management platform
导读
本文聚焦 Kanidm 身份管理平台的 Python 客户端 pykanidm 中用于缓存用户认证令牌(User Auth Token)的TokenStore模块:它负责把登录成功后获得的 JWS(JSON Web Signature)格式会话令牌,按“服务器实例 + 用户名”的维度持久化到本地磁盘,并在后续请求中解析、读取与校验。读完本文,你将掌握令牌缓存的默认存储路径、磁盘 JSON 结构、JWS 头/载荷/签名的解析细节、TokenStore的加载/保存/校验/查询 API,以及它与KanidmClient认证流程和 Rust 官方 CLI 之间的对应关系,并能在自己的脚本中直接复用这些能力。
一、TokenStore 在 pykanidm 中的角色
Kanidm 的服务端在用户完成认证(密码、TOTP、Passkey、安全密钥等)后,会签发一个 JWS 格式的会话令牌(User Auth Token,简称 UAT)。客户端后续所有 API 调用都通过Authorization: Bearer <token>携带该令牌完成鉴权。
pykanidm 的文档站(pykanidm/mkdocs.yml中 “Token Storage” 一节)通过 pykanidm/docs/tokenstore.md 中的 mkdocstrings 指令(::: kanidm.tokens)自动生成kanidm.tokens模块的 API 参考。该模块正是负责在本地缓存这些令牌的组件:
- 登录成功后自动把令牌写入本地缓存文件;
- 程序重启后可以从缓存恢复会话,避免重复交互式登录;
- 通过本地解析 JWS 得到令牌的过期时间、权限限额等元信息,无需每次都请求服务端。
值得强调的是,这并非 Python 侧独有的设计:Rust 官方 CLI 同样使用同一份缓存文件,其常量定义于 proto/src/constants.rs:
/// The default location for the `kanidm` CLI tool's token cache. pub const CLIENT_TOKEN_CACHE: &str = "~/.cache/kanidm_tokens";也就是说,pykanidm 与官方kanidmCLI 共享同一份令牌缓存,这是两套客户端可以无缝交替使用的关键前提。
二、缓存文件的默认位置与磁盘结构
2.1 默认路径的确定逻辑
令牌缓存文件的路径由 pykanidm/kanidm/init.py 定义:
XDG_CACHE_HOME = ( Path(os.getenv("LOCALAPPDATA", "~/AppData/Local")) / "cache" if platform.system() == "Windows" else Path(os.getenv("XDG_CACHE_HOME", "~/.cache")) ) TOKEN_PATH = XDG_CACHE_HOME / "kanidm_tokens"各平台默认解析结果如下:
| 平台 | 环境变量 | 默认令牌缓存路径 |
|---|---|---|
| Linux / macOS(Unix 系) | XDG_CACHE_HOME | ~/.cache/kanidm_tokens |
| Windows | LOCALAPPDATA | %LOCALAPPDATA%\cache\kanidm_tokens |
Linux/macOS 下这与 Rust 端CLIENT_TOKEN_CACHE = "~/.cache/kanidm_tokens"(见 proto/src/constants.rs)完全一致,印证了双端共享缓存的事实。在 Windows 上 pykanidm 将缓存文件放置在LOCALAPPDATA\cache之下,与 Unix 的 XDG 惯例保持了对等语义。
2.2 磁盘上的 JSON 结构
TokenStore以美观缩进的 JSON 写入磁盘(save方法使用model_dump_json(indent=2))。整体结构是“实例 → 用户名 → 令牌”的三层嵌套:
{ "instances": { "": { "keys": {}, "tokens": { "idm_admin": "<JWS 令牌字符串>" } }, "my-kanidm-instance": { "keys": { "<key_id>": { "kty": "EC", "crv": "P-256", "x": "...", "y": "...", "alg": "ES256", "use": "sig" } }, "tokens": { "idm_admin@example.com": "<JWS 令牌字符串>" } } } }各字段含义:
instances:顶层字典,键是服务器实例名(默认实例为空字符串"");- 每个实例对应一个
ConfigInstance,包含两个子字典:keys:该实例签发令牌所用的公钥 JWK 集合(Dict[str, Dict[str, Any]]),Rust 端正是用它们来做 ES256 签名验签;tokens:用户名/SPN → JWS 令牌字符串的映射(Dict[str, str])。
在 Python 侧,这一结构由三个 pydantic 模型精确描述(见 pykanidm/kanidm/tokens.py):
class ConfigInstance(BaseModel): """Configuration Instance""" keys: Dict[str, Dict[str, Any]] = Field(dict()) tokens: Dict[str, str] = Field(dict()) class TokenStore(BaseModel): """Represents the user auth tokens, so we can load them from the user store""" instances: Dict[str, ConfigInstance] = Field({"": ConfigInstance.model_construct()})注意TokenStore.instances的默认值内置了一个空字符串键的ConfigInstance,这意味着默认实例无需显式命名即可直接使用。
三、JWS 令牌的解析原理
缓存里存的不是明文 JSON,而是紧凑序列化的 JWS(形如header.payload.signature的三段 base64url 字符串)。kanidm.tokens模块提供了完整的本地解析器(不依赖服务端),让调用方可以直接读取令牌内的身份与限额信息。
3.1 JWS 头:JWSHeader
class JWSHeader(BaseModel): class JWSHeaderJWK(BaseModel): kty: str crv: str x: str y: str alg: str use: str alg: str typ: str jwk: JWSHeaderJWK头中包含alg(签名算法,Kanidm 使用ES256)、typ(JWT)以及内嵌的签名公钥jwk(椭圆曲线 P-256 的kty/crv/x/y坐标)。测试用例 pykanidm/tests/test_jwt.py 中给出的真实头示例为:
{ "alg": "ES256", "jwk": { "kty": "EC", "crv": "P-256", "x": "mJA8-MD_xTqApfIOglZm5rzEhhCxCv4qdSixlcWT7fk", "y": "7-2VCncHw4AuYRiaZXOahUtF1A6H7wyLkQmEzGnKJJs", "alg": "ES256", "use": "sig" }, "typ": "JWT" }3.2 载荷:JWSPayload
载荷是令牌的“身份卡片”,其字段定义(pykanidm/kanidm/tokens.py)与 Rust 端UserAuthToken结构(见 proto/src/internal/token.rs)遥相呼应:
| 字段 | 类型 | 含义 |
|---|---|---|
session_id | str | 会话唯一 ID(UUID) |
auth_type | str | 认证方式(如generatedpassword) |
expiry | List[int] | 过期时间,编码为[年, 年内第几天, 秒, 纳秒]四元组 |
uuid | str | 用户 UUID |
name | str | 用户名 |
displayname | str | 显示名 |
spn | str | 服务主体名,如idm_admin@localhost |
mail_primary | Optional[str] | 主邮箱(可空) |
lim_uidx | bool | 用户索引限额开关 |
lim_rmax | int | 组上限(测试样例中为 128) |
lim_pmax | int | 人员上限(样例中为 256) |
lim_fmax | int | 服务账号上限(样例中为 32) |
3.3 解析实现:JWS与 base64url 补位
JWS.parse完成三段式的切分与解码(pykanidm/kanidm/tokens.py):
- 校验字符串中恰好包含两个
.,切分出 header / payload / signature 三段; - 对每段调用
_pad_base64url_segment补=填充到 4 的倍数(base64 解码长度要求),因为 JWS 的 base64url 编码省略了填充符; - 分别用
base64.urlsafe_b64decode解码,header 与 payload 用 JSON 解析后交给 pydantic 模型JWSHeader/JWSPayload校验,signature 保留为原始字节。
@classmethod def _pad_base64url_segment(cls, segment: str) -> str: """Pad a base64url segment to a decodable length.""" return segment + "=" * (-len(segment) % 4)解析失败的边界情况也有处理:不含.或段数不为 3 时抛出ValueError("Invalid number of segments...")。
3.4 过期时间的还原:expiry_datetime
JWSPayload提供了一个expiry_datetime属性,把服务端编码的[year, day_of_year, seconds, nanos]转换为真实的 UTCdatetime(pykanidm/kanidm/tokens.py):
@property def expiry_datetime(self) -> datetime: """parse the expiry and return a datetime object""" year, day, seconds, _ = self.expiry retval = datetime(year=year, month=1, day=1, second=0, hour=0, tzinfo=timezone.utc) # day - 1 because we're already starting at day 1 retval += timedelta(days=day - 1, seconds=seconds) return retval测试 pykanidm/tests/test_jwt.py 用真实令牌验证了这一换算:expiry = [2022, 265, 28366, 802525000]应还原为2022-09-22 09:25:23 UTC(265 天 = 9 月 22 日,再叠加 28366 秒)。这也解释了源码注释里的 TODO——expiry四元组格式正是服务端timecrate 序列化的年/日/秒/纳秒表示。
四、TokenStore 的核心 API 与用法
4.1save:写入磁盘
def save(self, filepath: Path = TOKEN_PATH) -> None: """saves the cached tokens to disk""" data = self.model_dump_json(indent=2) with filepath.expanduser().resolve().open(mode="w", encoding="utf-8") as file_handle: file_handle.write(data)默认写入TOKEN_PATH(即~/.cache/kanidm_tokens),也可显式传入任意Path重定向(例如测试中写入临时目录)。写入前会先expanduser().resolve()展开~并解析为绝对路径。
4.2load:从磁盘加载
def load(self, overwrite: bool = True, filepath: Path = TOKEN_PATH) -> None: """Loads the tokens from from the store and caches them in memory - by default from the local user's store path, but you can point it at any file path. If overwrite=False, then it will add them to the existing in-memory store"""filepath不存在时,返回一个空的TokenStore(model_validate({})),而不是报错;overwrite=True(默认)时用磁盘内容整体替换内存中的 store;overwrite=False时执行“朴素合并”:仅把磁盘上尚不存在于内存的实例补充进来,已有实例不会被覆盖;- 源码注释中保留了一个 TODO:
# self.validate_tokens(),说明加载后的自动格式校验尚未启用。
4.3validate_tokens:批量格式校验
def validate_tokens(self) -> None: """validates the JWS tokens for format, not their signature - PRs welcome""" for instance_name, instance in self.instances.items(): for username, token in instance.tokens.items(): logging.debug("Parsing instance=%s username=%s", instance_name, username) logging.debug(JWS(token))遍历所有实例与用户名,逐个构造JWS对象。需要特别说明的是:它只校验 JWS 的格式与字段完整性,不验证签名——模块源码的 docstring 明确写道 “validates the JWS tokens for format, not their signature - PRs welcome”。真正的签名验签发生在 Rust 端(tools/cli/src/cli/session.rs 的valid_uats,用实例缓存的公钥 JWK 通过JwsEs256Verifier完成),Python 侧目前定位为轻量解析。
4.4token_info:按用户名取令牌信息
def token_info(self, username: str, instance: Optional[str] = None) -> Optional[JWSPayload]: """grabs a token and returns a complex object object"""这是日常使用最频繁的查询入口:
instance省略时按默认实例""查找;- 找不到实例、实例下没有
tokens字段、或该用户名无令牌时,均返回None(并输出对应级别的日志); - 命中时把原始 JWS 字符串解析为
JWS对象,返回其payload(一个JWSPayload),调用方即可读取spn、displayname、expiry_datetime、lim_*等字段。
五、与 KanidmClient 认证流程的配合
TokenStore虽不在KanidmClient内部强制调用,但它是完整的“登录 → 缓存 → 复用”闭环中的存储层。结合 pykanidm/tests/test_authenticate.py 可以看到典型用法——认证测试前先加载本地缓存令牌:
@pytest.mark.network @pytest.mark.asyncio async def test_authenticate_with_token(client_configfile: KanidmClient) -> None: """tests auth with a token, needs to have a valid token in your local cache""" ... tokens = TokenStore.model_validate({}) tokens.load()整体流程可归纳为:
- 用户通过
KanidmClient.auth_init→auth_begin→auth_step_password等步骤完成认证,成功令牌被写入client.config.auth_token(见 pykanidm/kanidm/init.py); - 应用层把该令牌保存进
TokenStore(按实例与用户名组织)并调用save()落盘; - 下次启动时
TokenStore.load()恢复内存缓存,再用token_info()检查令牌是否过期、决定是否需要重新认证; - 令牌随每次 API 请求以
Authorization: Bearer <token>头发送(_token_headers属性,见 pykanidm/kanidm/init.py)。
Rust 端LoginOpt::exec(tools/cli/src/cli/session.rs)展示了同一流程的完整实现:认证成功后读取缓存 → 取或建实例 → 用keys中的 JWK 验签 → 以spn为键写入tokens→write_tokens落盘;LogoutOpt与SessionOpt::Cleanup则分别负责删除会话与清理过期令牌。
六、实战示例:解析缓存中的令牌
下面给出一个可直接运行的完整示例,读取本地缓存并打印idm_admin的令牌信息(数据与断言来源于 pykanidm/tests/test_jwt.py):
from kanidm.tokens import JWS, TokenStore # 1) 从默认路径 (~/.cache/kanidm_tokens) 加载令牌缓存 token_store = TokenStore.model_validate({}) token_store.load() # 2) 查询某个用户的令牌 info = token_store.token_info("idm_admin") if info is not None: print(f"SPN: {info.spn}") print(f"DisplayName: {info.displayname}") print(f"AuthType: {info.auth_type}") print(f"Expires(UTC): {info.expiry_datetime}") print(f"Limits: rmax={info.lim_rmax} pmax={info.lim_pmax} fmax={info.lim_fmax}") else: print("未找到该用户的缓存令牌,请先登录") # 3) 直接解析一段原始 JWS 字符串 test_jws = JWS("<header>.<payload>.<signature>") print(test_jws.header.alg) # ES256 print(test_jws.payload.name) # 用户名也可以不通过TokenStore,直接构造与测试相同的字典来体验数据模型:
data = { "instances": { "": { "tokens": { "idm_admin": "<JWS 令牌字符串>", } } } } token_store = TokenStore.model_validate(data) print(token_store.model_dump_json(indent=2)) # 复刻磁盘格式 info = token_store.token_info("idm_admin") assert info is not None print(info.expiry_datetime)七、安全注意事项
- 缓存文件是明文 JSON:
~/.cache/kanidm_tokens中保存的是可直接用于鉴权的 JWS 令牌,等同于会话凭证。Rust 端写入时会通过umask(0o177)临时收紧文件权限(见 tools/cli/src/cli/session.rs),避免同组/其他用户读取;Python 侧save()直接以默认权限写文件,因此务必保证该文件所在目录仅本人可访问; - 令牌会过期:
JWSPayload.expiry_datetime可用来在本地判断令牌是否仍然有效,避免拿着过期令牌去请求服务端再被拒绝;如需更严格的生命周期管理,可参考 Rust 端cleanup(tools/cli/src/cli/session.rs)按过期时间清理缓存; - 本地解析不等于验签:
validate_tokens/JWS只做格式解析,如需确认令牌确由目标服务器签发,仍需通过服务端接口或持有对应公钥做 ES256 验签。
结语
TokenStore 是 pykanidm 中负责用户认证令牌本地缓存的核心模块:它以~/.cache/kanidm_tokens(Windows 为%LOCALAPPDATA%\cache\kanidm_tokens)为默认落盘路径,用instances → keys/tokens的三层 JSON 结构按实例与用户名组织缓存,并提供save / load / validate_tokens / token_info四个核心方法,配合JWS解析器可无服务端地读取令牌的身份、SPN 与过期时间。由于它与 Rust 官方 CLI 共享同一份缓存文件与相似的实现语义,开发者完全可以借助 pykanidm 在脚本、自动化任务或运维工具中复用 CLI 已建立的登录会话,构建“一次登录、多处复用”的免交互集成方案。更多细节可直接阅读 pykanidm/kanidm/tokens.py 源码与 pykanidm/tests/test_jwt.py 测试用例。
- 后端
- 认证鉴权
- 安全
【免费下载链接】kanidm
Kanidm: A simple, secure, and fast identity management platform
相关推荐
Librespot设备认证令牌管理:安全存储与刷新机制
Librespot设备认证令牌管理:安全存储与刷新机制 在音乐流媒体应用开发中,设备认证令牌(Token)的安全管理直接关系到用户账户安全和服务可用性。Libr
音频处理KK-HF_Patch完整配置指南:三步解决Koikatu游戏体验增强的终极方案
KK HF_Patch完整配置指南:三步解决Koikatu游戏体验增强的终极方案 你是否曾经为Koikatu或Koikatsu Party游戏的语言障碍、功能限
游戏开发开发工具Optimize-Offline安全优化:如何通过注册表调整增强Windows 10隐私保护
Optimize Offline安全优化:如何通过注册表调整增强Windows 10隐私保护 在当今数字时代,Windows 10用户的隐私保护变得越来越重要。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考