本文记录 jsonwebtoken 9.0.3 适配仓颉(Cangjie)的完整过程。适配目标是得到一个可以被仓颉项目直接依赖的跨端三方库,并在本地终端中通过真实的 JWT 签名、验证、解码、声明校验、过期处理和错误路径验证库的输出。
项目地址: AtomGit/nutpi_cj/node-jsonwebtoken
上游仓库: auth0/node-jsonwebtoken
开发工具: 华为云码道
一、背景
1.1 为什么适配 jsonwebtoken
jsonwebtoken 是 Node.js 生态中使用广泛的 JSON Web Token 实现,提供 sign、verify 和 decode 等入口,用于生成 compact JWT、校验签名以及读取 token 内容。JavaScript 版本接收动态 payload、secret/key、options 和 callback,运行时还会根据算法、密钥类型和声明内容选择不同的校验路径。
仓颉强调静态类型、显式异常和可检查的模块边界。直接把 JavaScript 源码逐行翻译成仓颉,无法自然表达动态 JSON、可选声明和多种密钥输入,因此本适配重新组织为仓颉值模型、显式选项类、明确的验证结果和可捕获错误。
本项目保留熟悉的 JWT 使用概念:sign 生成 token,verify 返回 payload/header/signature,decode 只解码不验签,并额外提供 signJson 和 verifyJson 方便以 JSON 文本作为边界。这样熟悉 jsonwebtoken 的开发者可以沿用原有思路,同时获得仓颉编译器的参数检查。
1.2 跨端三方库的适配边界
当前项目提供的是仓颉跨端库,不绑定某一个 UI 框架或操作系统。适配边界可以概括为:
| 边界 | 仓颉实现 |
|---|---|
| 包管理 | cjpm.toml、cjpm.lock、cjpm build 和静态库输出 |
| JSON 模型 | JwtValue 表达 null、布尔、数字、字符串、数组和对象 |
| 编码 | RFC 4648 Base64URL 无填充编解码 |
| 对称认证 | 纯仓颉 HMAC-SHA-256、HMAC-SHA-384 和 HMAC-SHA-512 |
| JWT | HS256、HS384、HS512 Compact JWT 和 none 无签名 token |
| 声明校验 | iat、exp、nbf、aud、iss、sub、jti、nonce、maxAge |
| 错误模型 | JwtError,包含格式、算法、签名、声明和时间校验错误 |
| 交付形态 | 仓颉静态库 libjsonwebtoken_cangjie.a 和接口文件 jsonwebtoken_cangjie.cjo |
| 验证方式 | test/main.cj 独立编译测试和 example/example.cj 终端示例 |
这里没有 HAP 构建、ArkTS 桥接和真机系统能力验收。当前实现以零外部依赖、可重复运行和可观察输出为目标;RSA、PSS 和 ECDSA 需要目标平台密码库接入,本次仓颉包会明确返回不支持错误,不生成伪造签名。
1.3 适配后的能力
当前导出的仓颉 API 覆盖以下能力:
- JSON 值构造:
jsonNull、jsonBool、jsonNumber、jsonString、jsonObject和jsonArray; - JWT 生成:
sign和signJson,支持算法、时间、声明、kid和none选项; - JWT 验证:
verify和verifyJson,支持算法白名单、声明匹配、过期/生效时间、时钟容差、ignoreExpiration、ignoreNotBefore和maxAge; - JWT 解码:
decode返回 header、payload 和 signature,不把解码误认为签名验证; - JSON 处理:
parseJson和stringify,可处理嵌套数组、对象、布尔、数字和字符串; - 结果与错误:
JwtDecoded、JwtVerification和JwtError,示例会直接打印真实 token、payload、signature 和失败信息。
二、实现路线图
第 1 阶段:盘点 API ── 对照 jsonwebtoken 的 sign/verify/decode 生命周期,确定仓颉类型和算法边界
第 2 阶段:建立 cjpm 工程 ── 固定包名、版本、源码目录和静态库输出
第 3 阶段:类型映射 ── 用 enum、class、Option 和显式异常替换动态 payload/options
第 4 阶段:实现 JWT ── 完成 JSON、Base64URL、HMAC、claims 注入和验证错误处理
第 5 阶段:测试与核对 ── 编译测试入口,执行真实签名、解码、声明和失败路径校验
第 6 阶段:终端演示 ── 输出所有已实现功能、算法结果、错误信息和运行效果图
每个阶段都有可检查的产物:工程阶段得到 cjpm.toml 和 cjpm.lock,实现阶段得到 .cjpm-target/jsonwebtoken_cangjie,验证阶段得到固定测试输出,最终示例则输出真实计算出的 token、header、payload、signature 和错误结果。
三、逐步实现过程
第 1 阶段:盘点原库 API 和仓颉工程边界
1.1 识别动态 API
jsonwebtoken 的 JavaScript API 接受对象、字符串、Buffer、密钥对象和多种 options 组合。仓颉没有与这些 JavaScript 动态联合类型完全等价的边界,因此本适配先筛选可以由静态类型稳定表达的核心流程,再为这些流程设计清晰签名。
payload 使用递归的 JwtValue 表达;secret 使用 String;sign 和 verify 的可选配置分别使用 JwtSignOptions 与 JwtVerifyOptions;解码和验证结果使用 JwtDecoded 与 JwtVerification。格式错误、算法错误和校验失败统一通过 JwtError 抛出,调用方可以在 catch 中读取真实错误文本。
1.2 固定仓颉项目结构
以下目录树按照当前 jsonwebtoken 项目的仓颉适配文件整理,只展示仓颉相关文件;原始 Node.js 源码、测试、lib/ 和其他目录保持原位置:
node-jsonwebtoken/
├── cjpm.toml
├── cjpm.lock
├── README.Cangjie.md
├── README.Cangjie_CN.md
├── src/
│ └── jsonwebtoken.cj
├── test/
│ └── main.cj
└── example/
├── cjpm.toml
├── cjpm.lock
└── example.cj
源码集中在 src/jsonwebtoken.cj,按 JSON、Base64URL、摘要、JWT 生成和验证顺序组织。测试入口位于 test/main.cj,示例工程位于 example/,两者都独立于库源码。根包的 target-dir 是 .cjpm-target/jsonwebtoken_cangjie,在 src/ 扫描范围之外;示例使用 src-dir = ".",target-dir 放在 example/ 外部,因此构建输出不会再次被当作源码扫描。
第 2 阶段:创建 cjpm 工程
根目录 cjpm.toml 使用静态库输出类型,并固定源码目录和构建目录:
[package]
cjc-version = "1.1.3"
name = "jsonwebtoken_cangjie"
organization = ""
description = "Cangjie implementation of JSON Web Token signing, verification and decoding"
version = "9.0.3"
src-dir = "src"
target-dir = ".cjpm-target/jsonwebtoken_cangjie"
output-type = "static"
package-configuration = {}
[dependencies]
示例工程使用本地路径依赖,并把构建目录放在 example 外部:
[package]
cjc-version = "1.1.3"
name = "jsonwebtoken_example"
organization = ""
description = "Observable JSON Web Token feature demonstration"
version = "1.0.0"
src-dir = "."
target-dir = "../.cjpm-target/jsonwebtoken_example"
output-type = "executable"
package-configuration = {}
[dependencies]
jsonwebtoken_cangjie = { path = ".." }
包版本沿用当前 jsonwebtoken 版本,包名使用 jsonwebtoken_cangjie。构建成功后,最重要的两个库产物是:
.cjpm-target/jsonwebtoken_cangjie/release/jsonwebtoken_cangjie/libjsonwebtoken_cangjie.a
.cjpm-target/jsonwebtoken_cangjie/release/jsonwebtoken_cangjie/jsonwebtoken_cangjie.cjo
其中 .a 用于链接,.cjo 提供编译器需要的接口信息。示例可执行文件写入 .cjpm-target/jsonwebtoken_example,验证完成后清理这些目录,不提交构建产物。
第 3 阶段:把 JavaScript 动态输入映射为仓颉类型
3.1 用 JwtValue 表达 JSON payload
仓颉边界不强制调用方引入新的 JSON 依赖,payload 可以通过类型构造器表达:
let payload = jsonObject([
("sub", jsonString("alice")),
("role", jsonString("admin")),
("permissions", jsonArray([jsonString("read"), jsonString("write")])),
("active", jsonBool(true))
])
let token = sign(payload, "demo-secret",
options: JwtSignOptions(issuedAt: Some(1700000000), expiresIn: Some(3600)))
println(token)
JwtValue 同时支持嵌套数组和对象;stringify 将它序列化为紧凑 JSON,parseJson 可以从 JSON 文本恢复同一值模型。signJson 和 verifyJson 则为已有 JSON 字符串的调用方提供直接入口。
3.2 用选项类表达 claims 和验证约束
JavaScript options 中的动态字段在仓颉中变成显式的 Option<T> 成员:
let options = JwtSignOptions(
issuedAt: Some(1700000000), expiresIn: Some(3600), notBefore: Some(-5),
audience: Some("users"), issuer: Some("example-service"),
subject: Some("alice"), jwtid: Some("example-1"), nonce: Some("nonce-1"),
keyid: Some("demo-key"))
let token = sign(payload, "demo-secret", options: options)
let verified = verify(token, "demo-secret", options: JwtVerifyOptions(
clockTimestamp: Some(1700000100), audience: Some("users"),
issuer: Some("example-service"), subject: Some("alice"),
jwtid: Some("example-1"), nonce: Some("nonce-1")))
println(stringify(verified.payload))
签名时会把 iat、exp、nbf 和其他声明写入 payload;验证时会根据指定时钟检查生效时间、过期时间、最大年龄和声明匹配。示例使用固定 issuedAt 和 clockTimestamp,使每次运行得到可复核的结果。
3.3 用 JwtError 表示失败
JavaScript jsonwebtoken 会把错误密钥、错误算法、过期 token 和声明不匹配分别返回错误。仓颉版本把这些失败收敛到可捕获的 JwtError:
try {
verify(token, "wrong-secret")
} catch (error: JwtError) {
println(error.message)
}
这种错误模型可以覆盖 malformed token、invalid signature、jwt expired、jwt audience invalid 和不支持算法。示例会主动触发错误密钥、过期时间和 audience 不匹配,并把错误文本打印到终端,而不是只输出一个断言通过标记。
第 4 阶段:实现源码
4.1 文件 src/jsonwebtoken.cj
源码按 JWT 生命周期组织:先由 JsonParser 解析 JSON,再用 stringify 生成 header/payload 文本;随后通过 Base64URL 编码组成 signing input;最后使用纯仓颉 HMAC 计算签名。
sign 默认使用 HS256,也支持 HS384、HS512 和 none;header 会包含 alg、typ,配置 keyid 时追加 kid。对象 payload 会注入时间和标准声明,字符串等非对象 payload 可以用于无声明 token。decode 只负责拆分和解码三段内容,返回 header、payload 和 signature,不把解码结果误认为已验签。
4.2 Base64URL 和 HMAC
JWT 的 header、payload 和 signature 使用无填充 Base64URL。仓颉实现直接处理 Array<UInt8>,不依赖 Node Buffer。HMAC-SHA-256、HMAC-SHA-384 和 HMAC-SHA-512 的摘要、补位、块处理和最终编码都在 src/jsonwebtoken.cj 中完成;示例打印的三个算法结果可直接用 Node crypto.createHmac 交叉验证。
验证时会重新计算 signing input 的签名,并与 token 的第三段比较。none 只允许空 secret 和空 signature,避免把未签名 token 当作有密钥的 token 接受。
4.3 非对称算法和平台边界
原 Node.js jsonwebtoken 还支持 RSA、RSA-PSS 和 ECDSA。当前仓颉包没有引入平台密码库或 OpenSSL 外部链接,因此没有伪造这些算法的兼容实现;调用 RS256、PS256 或 ES256 等算法会明确抛出 Unsupported algorithm。如果目标平台需要非对称 JWT,应在平台提供真实密码学 API 后扩展同一套 JwtSignOptions 和 JwtVerifyOptions 边界。
第 5 阶段:建立自动化测试
test/main.cj 是一个直接链接仓颉静态库的可执行测试入口,覆盖以下行为:
| 测试组 | 示例 |
|---|---|
| JSON 与编码 | 对象、数组、布尔、数字、字符串、Base64URL 和 JSON 往返 |
| 签名算法 | HS256、HS384、HS512 的 token 生成和验证 |
| JWT 声明 | iat、exp、nbf、aud、iss、sub、jti、nonce 和 kid |
| 解码结果 | header、payload 和 signature 的实际字段 |
| 错误 | 错误 secret、过期时间和 none 算法校验 |
断言失败会抛出异常并以非零状态退出;全部通过时输出固定标记:
JWT CANGJIE TESTS: sign, verify, decode, claims, expiry, errors and none all passed
示例工程则负责展示完整中间结果,测试程序负责断言库调用,两者共同覆盖编译期 API 和运行时签名、解码、声明校验及错误行为。
第 6 阶段:核对 jsonwebtoken 适配 API
当前仓颉公开入口按功能核对如下:
JSON values: JwtValue, jsonNull, jsonBool, jsonNumber, jsonString, jsonObject, jsonArray
JWT operations: sign, signJson, verify, verifyJson, decode, parseJson, stringify
Options/results: JwtSignOptions, JwtVerifyOptions, JwtDecoded, JwtVerification, JwtError
Algorithms: HS256, HS384, HS512, none
这个检查说明当前仓颉适配公开入口与 README 和示例一致,不代表它已经覆盖 JavaScript jsonwebtoken 的回调式 API、Buffer/KeyObject 输入和 RSA/PSS/ECDSA 平台实现。
四、完整验证步骤
以下命令均以仓库根目录 node-jsonwebtoken/ 为当前目录。先确认本机已经安装仓颉 SDK:
echo "$CANGJIE_HOME"
cjc --version
cjpm --version
然后构建静态库并检查依赖:
cjpm check
cjpm build
编译并运行测试入口:
cjc test/main.cj \
--import-path .cjpm-target/jsonwebtoken_cangjie/release/jsonwebtoken_cangjie \
-L .cjpm-target/jsonwebtoken_cangjie/release/jsonwebtoken_cangjie \
-l jsonwebtoken_cangjie \
-o /tmp/jsonwebtoken-cangjie-test
DYLD_LIBRARY_PATH="$CANGJIE_HOME/runtime/lib/darwin_aarch64_cjnative" \
/tmp/jsonwebtoken-cangjie-test
再构建并运行独立的终端示例工程:
cd example
cjpm check
cjpm run
示例会打印真实计算结果,而不是只打印断言通过:
1) sign(payload, secret, options)
token = eyJhbGciOiJIUzI1NiIs...
HMAC algorithm outputs
HS256: signature=bV57e87Mb1yXVSgIxpNbMTQkJzPioO8R-wwdd75bClI, payload={...}
HS384: signature=gyU28bzsnG5l0LIgGYCzHrYY_Zfg-ze4HSdu0PbdHcelI1gGkHJt2HXEuVupzu8Q, payload={...}
HS512: signature=K1uinoNOztRtdgOYBLzpsLw08H-Z5RvgSDl-421Zc_7AZEuKIxGBU5cLZVUrwDXm5-801OakwAV2fFm55K8Z-Q, payload={...}
2) decode(token) without verification
decoded.header = {"alg":"HS256","typ":"JWT","kid":"demo-key"}
decoded.payload = {"sub":"alice",...,"nonce":"nonce-1"}
decoded.signature = e5-AuZ6julNG4wMk19bDJD2xhxa_wBab7smNiZzu8lo
3) verify(token, secret, claim checks)
verified.payload = {...}
verified.header = {"alg":"HS256","typ":"JWT","kid":"demo-key"}
verified.signature = e5-AuZ6julNG4wMk19bDJD2xhxa_wBab7smNiZzu8lo
4) verification failures are returned as real errors
wrong secret: invalid signature
expired clock: jwt expired
audience mismatch: jwt audience invalid
5) signJson / verifyJson convenience
json payload = {"message":"hello","count":2}
6) unsigned token (algorithm none)
unsigned payload = "public"
All JWT feature results were displayed
示例使用 target-dir = "../.cjpm-target/jsonwebtoken_example",位于 src-dir = "." 的扫描范围之外,因此运行 cjpm run 时不会出现把构建输出目录当作源码扫描目录的警告。验证结束后清理 .cjpm-target,确认它没有加入 Git。
五、运行效果
下面的效果图来自 example/example.cj 的实际终端运行,包含构建后的 jsonwebtoken Cangjie 签名、解码、验证、错误处理和 none 算法结果。

这张图可以作为文章或项目说明中的验收证据:终端同时展示了解码后的 header、payload、signature,验证后的内容,错误 secret、过期时间、audience 不匹配,以及 JSON convenience 和无签名 token 的结果。文本都来自仓颉示例程序实时调用 jsonwebtoken_cangjie 后的返回值,不是预先写死的展示文本,也不是只显示断言通过。
六、关键决策说明
6.1 为什么使用 JwtValue 和显式 options
仓颉适配的价值在于让调用方在编译期知道 payload 和 options 的数据形状。若所有内容都退化成无类型字符串,虽然表面上接近 JavaScript,却会失去数组、对象、布尔和数字的结构检查。因此项目使用递归 enum 表达 JSON,使用 Option<T> 表达可选声明,让常见调用保持可读、可检查和可预测。
6.2 为什么保留 sign、verify、decode 名称
保留 jsonwebtoken 的三个核心名称可以降低迁移成本。熟悉 Node.js jsonwebtoken 的开发者只需要适应仓颉的命名参数、JwtValue 构造和异常捕获,不必重新记忆一套完全不同的 token API。verify 直接返回完整验证结果,使终端示例可以同时展示 payload、header 和 signature。
6.3 为什么把测试和示例分开
测试程序负责断言签名算法、claims 和错误路径,适合自动化执行;示例工程负责展示 token、解码结果和真实失败信息,适合在终端中观察和截图。两者都使用同一个 jsonwebtoken_cangjie 静态库,因此效果图来自库的实际运行输出,而不是模拟文本。
七、兼容边界和遗留问题
JavaScript jsonwebtoken 与仓颉以及目标平台的密钥模型不同,以下限制需要在接入时提前了解:
- 当前实现支持 HS256、HS384、HS512 和
none;RSA、RSA-PSS、ECDSA 和其他非对称算法尚未接入平台密码库。 - secret 当前使用 UTF-8
String,没有映射 Node.js Buffer、KeyObject、PEM 私钥或异步 secret callback。 - payload 使用仓颉
JwtValue或 JSON 文本,支持常见 JSON 类型;JavaScript 特有的 Buffer payload、Date 对象和动态原型语义不在范围内。 audience、issuer、subject、jwtid和nonce以字符串选项表达;正则 audience、数组 audience 和复杂 callback 选项尚未提供。test/main.cj是直接链接静态库的可执行测试入口,仓颉cjpm test不会把这个普通main.cj自动当作 unittest macro 用例;文章提供了可复现的cjc编译命令。- 当前没有 HAP 构建、ArkTS 桥接、真机系统能力验收。构建产物位于
.cjpm-target,验证结束后不提交该目录。
这些不是构建错误。只要 cjpm check、cjpm build 成功,测试入口输出固定通过标记,示例输出真实 token 和错误信息,就说明当前版本已经完成本地编译和行为验收。对于暂时无法一比一映射的非对称密码学能力,项目通过显式错误保持行为可解释,避免把不安全的伪实现交给业务代码。
八、接入方式
在另一个仓颉项目中,可以将本仓库作为本地依赖或 AtomGit 源码依赖使用。最直接的本地验证方式是:
[dependencies]
jsonwebtoken_cangjie = { path = "../node-jsonwebtoken" }
调用代码只需要导入仓颉包:
import jsonwebtoken_cangjie.*
main(): Int64 {
let payload = jsonObject([
("sub", jsonString("cangjie-user")),
("role", jsonString("reader"))
])
let token = sign(payload, "demo-secret")
let result = verify(token, "demo-secret")
println(stringify(result.payload))
return 0
}
如果使用远程仓库管理依赖,则先从 AtomGit/nutpi_cj/node-jsonwebtoken 获取固定版本,再由业务工程统一构建。跨端项目可以复用相同的 payload、claims、签名和错误处理思路;具体的 RSA/ECDSA 密钥存储、异步 secret 获取和系统密码库接入由宿主平台负责。
九、总结
本次适配完成了 jsonwebtoken 核心 token 模型到仓颉静态类型模型的迁移:payload 使用 JwtValue,签名和验证选项使用显式类,Base64URL 和 HMAC 使用纯仓颉实现,时间声明和标准 claims 通过 Option<T> 表达,失败路径统一使用 JwtError。
验证链路包括 cjpm check、cjpm build、测试入口编译、HS256/384/512 真实签名、header/payload/signature 解码、claims 校验、过期处理、错误密钥、audience 不匹配和 none token。最终效果图来自真实的仓颉可执行程序,能够直观看到三方库在签名、验证、解码、JSON convenience 和错误处理场景中的输出。
对于暂时无法一比一映射 JavaScript jsonwebtoken 的 RSA、PSS、ECDSA、Buffer/KeyObject 和异步 secret 语义,项目通过静态签名和明确错误保持行为可解释,方便后续接入目标平台真实密码学能力。
适配过程 FAQ
以下问题来自仓颉工程创建、
cjpm依赖、与 HarmonyOS 设备联调的实际流程。
Q1:cjpm 依赖解析失败或拉取超时?
- 检查
cjpm.toml中的依赖坐标与版本号; - 配置可用的仓颉包仓库镜像或代理;
- 删除
target、cjpm.lock后重新执行cjpm update/cjpm build。
Q2:编译报找不到仓颉 SDK 或 cjc 版本不匹配?
- 设置
CANGJIE_HOME指向仓颉 SDK 根目录; - 确认
cjc --version、cjpm --version与工程要求的版本一致; - 清理构建缓存后重新生成。
Q3:与 HarmonyOS 设备联调时安装失败?
- 用
hdc list targets确认设备在线; - 确认 HAP 已签名,
bundleName与签名 profile 一致; - 设备需加入调试 profile 的
deviceIds。
Q4:动态库或 FFI 符号找不到?
- 检查导出符号名称、ABI 与打包路径;
- 确认
.so已随 HAP 一起打包到libs/<abi>/; - 用
llvm-readelf -d检查依赖库是否齐全。
Q5:测试用例在设备上失败,但本机通过?
- 对比设备与主机的架构、SDK 版本和环境变量;
- 用
hdc hilog抓取运行日志; - 优先复现最小用例,确认是否为平台差异或时序问题。
十、参考资料
转载自 CSDN-专业IT技术社区



