Web RP demo —— 开发者指南¶
eID 平台的浏览器 RP 演示 —— 一个使用 Next.js(App Router、TypeScript)编写的
依赖方示例。它在浏览器中演示通过 eID Mongolia 手机应用完成 二维码 / РД 推送登录,
以及为 PDF 加盖合格电子签名的完整流程。源码:web/(对 Java 版 smartid-demo
中 WebDemo.java + index.html 的忠实移植 —— 相同的界面、相同的流程、相同的蒙古语文案)。
这是一个真正的 RP。 与 macOS/iOS 客户端不同,Web 应用自身持有 RP secret: Go RP-API(
/v3/*)的RP_API_SECRET仅在服务端 route handler 中使用 (web/src/lib/rpclient.ts)。浏览器永远看不到该 secret —— 所有调用都通过 Web 应用的公开/api/*路由代理。 RP 集成详情见 RP 集成;使用同一套/api/*的桌面客户端见 macOS desktop。
1. 演示内容¶
- 身份认证 —— 二维码(
/api/start)或按 РД / civil ID 推送 (/api/login-notify)。在手机应用中使用 PIN1 确认。服务端路由会对 rpChallenge 上的签名做密码学校验,随后从证书 subject 中提取姓名、civil ID 与documentNumber并返回(浏览器不解析证书)。 - PDF 签名 —— 选择 PDF,在客户端本地计算其 SHA-256 摘要,并在手机应用中
使用 PIN2(签名密钥、不可否认)完成签名。后端返回加盖签章(PAdES / PKCS#7 + 验证页)
的 PDF(
/api/sign-pdf-download)。 - 组织代表权 —— 自然人登录后,“以哪个组织身份继续”的选项会实时从登记册获取
(
/api/representations,相当于爱沙尼亚的 äriregister)。
“虚拟手机”模拟不可用。 Java 演示通过
VirtualPhone/PhoneCryptoJVM SDK 执行真实的门限 ECDSA。该密码学实现仅存在于 JVM 侧,因此/api/simulate-phone返回501(dev)/404(prod)(web/src/app/api/simulate-phone/route.ts)。 真实流程为:使用 eID Mongolia 应用确认二维码 / 推送。
2. 架构¶
浏览器(page.tsx、demo/page.tsx)
│ fetch /api/* (同源,不含 secret)
▼
Next.js route handler (web/src/app/api/**,服务端)
│ rpclient.ts: Authorization: Bearer <RP_API_SECRET>
▼
Go RP-API (/v3/*,RP_API_BASE)
Java 演示是在内置的轻量 HTTP 服务器中调用 RP-API。在本次移植中,每个后端辅助接口
都变成了一个 Next.js route handler,由服务端调用 Go RP-API(/v3/...)
(无需 CORS,逻辑与 Java 版一致)。协议契约与 Go DTO(server/internal/dto/*.go)
保持 1:1:响应中包含 sessionID、vc.value、result.endResult、
result.documentNumber、cert.value(base64 DER)、cert.certificateLevel。
secret 与 RP 标识存放位置¶
| 值 | 位置 | 说明 |
|---|---|---|
RP_API_SECRET |
env,仅服务端 | 由 rpclient.ts 的 authHeaders() 以 Authorization: Bearer 发送。并非 NEXT_PUBLIC_,因此不会进入客户端打包产物。若未设置则不添加该头(dev / 关闭 RP-auth)。 |
RP_UUID / RP_NAME |
rpclient.ts 中的常量 |
并非 env —— 而是编译期常量(RP_UUID = "2d87bd3a-…"、RP_NAME = "Demo Bank")。已预先注册在 relying_parties 中,重启后保持稳定。 |
rpChallenge |
服务端,随机生成 | 每个请求 64 字节随机数(base64)。以 sessionId 为键保存在 challengeStore 中,用于在 /status 校验基于 ACSP_V2 的 AUTH 签名。 |
pollToken(PII 闸门)。 由于sessionId会暴露在二维码中,仅凭它无法读取 姓名 / 登记号 / 签名。start/login-notify/sign-pdf-start的每个响应都会返回pollToken,而/api/status、/api/sign-pdf-download必须携带该令牌 (web/src/lib/pollTokenStore.ts)。
3. /api/* 路由¶
公开演示 RP 流程(page.tsx、demo/page.tsx、macOS 客户端)¶
| 路由 | 方法 | 作用 | 对应 Go RP-API 接口 |
|---|---|---|---|
/api/start |
POST | 启动匿名二维码会话 → {sessionId, qr, deviceLinkBase, vc, pollToken}(qr = sessionId) |
POST /v3/authentication/device-link/anonymous |
/api/login-notify |
POST | {register, callbackUrl} —— 按 РД / civil ID 推送 → {sessionId, vc, pollToken}。每个目标限流 60 秒 3 次 |
POST /v3/authentication/notification/etsi/{etsi} |
/api/status |
GET | ?sessionId=&pollToken= 长轮询(服务端保持约 1 秒)。COMPLETE/OK 时校验基于 rpChallenge 的 AUTH 签名,并从证书中提取 name/idNumber/documentNumber |
GET /v3/session/{id}?timeoutMs=1000 |
/api/sign-start |
POST | {etsi, doc} —— 文本文档签名(PIN2)推送 → {sessionId, vc, doc, pollToken}。限流 60 秒 3 次 |
POST /v3/signature/notification/etsi/{etsi} |
/api/sign-pdf-start |
POST | {etsi, digestB64, fileName, onBehalfOf?} —— 以 PIN2 对 PDF 的 SHA-256 摘要签名的会话 → {sessionId, vc, pollToken}。摘要必须为 32 字节;限流 60 秒 3 次 |
POST /v3/signature/notification/etsi/{etsi} |
/api/sign-pdf-download |
POST | multipart file + sessionId + pollToken → 加盖签章的 PDF 字节(application/pdf 附件) |
POST /v3/signature/stamp/{sessionId} |
/api/representations |
POST | {personId} —— 该个人可代表的 ACTIVE 组织 → {personEtsi, representations}。限流 60 秒 10 次 |
GET /v3/organization/representations/etsi/{personEtsi} |
/api/simulate-phone |
POST | 已停用 —— 501(dev)/ 404(prod)。需要真实的门限 ECDSA JVM SDK |
— |
/api/health |
GET | {status:"ok", service:"eidmongolia-web"} |
— |
/demo/live 代理流程(web/src/app/api/demo/*)¶
面向公开网站在线演示页面的轻量代理 —— 它返回的结构不同(响应字段为 snake_case),
但仍然经由 rpclient.ts 调用 RP-API /v3。
| 路由 | 方法 | 作用 |
|---|---|---|
/api/demo/auth/init |
POST | 匿名 device-link 认证 → {session_id, device_link_url, control_code, poll_token, expires_at}(device_link_url 即原始 sessionId) |
/api/demo/auth/poll |
GET | ?id=&poll_token= —— 轮询会话、校验 AUTH 签名并返回身份信息 |
/api/demo/sign/init |
POST | multipart file + x-eid-token 头(认证阶段获得的 documentNumber)—— 计算 PDF 的 SHA-256 并发起 PIN2 签名流程 → {session_id, document_hash, verification_code} |
/api/demo/sign/poll |
GET | ?id= + x-eid-token —— COMPLETE/OK 时返回分离式 ECDSA signature_hex;页面自身用 pdf-lib 将其嵌入 PDF(此处没有 /download) |
4. 运行¶
npm run dev 会在 :3000 启动 web,并在 :3001 启动管理控制台(见 CLAUDE.md)。
环境变量¶
| 变量 | 默认值 | 说明 |
|---|---|---|
RP_API_BASE |
— | Go RP-API 的基础 URL(仅服务端)。route handler 会自行追加 /v3。生产环境未设置时,rpclient.ts 会抛出错误 |
NEXT_PUBLIC_API_BASE |
— | 遗留回退项(兼容用)。当 RP_API_BASE 未设置时读取 |
RP_API_SECRET |
— | RP 共享密钥。需与 Go 侧的 SMARTID_RP_API_SECRET 一致。为空时不添加 Bearer(dev) |
NODE_ENV |
— | 为 production 时,simulate-phone 返回 404,且 RP_API_BASE 为必填 |
dev 回退。 若
RP_API_BASE与NEXT_PUBLIC_API_BASE均为空, 仅在 dev 环境会回退到http://localhost:8080/v3(rpclient.ts的apiBase())。 因此本地测试只需在:8080启动 Go 服务端即可。
完整的本地技术栈(Go API + web):
cd server && SMARTID_RP_API_SECRET= go run ./cmd/smartid # Go API :8080(关闭 RP-auth)
cd web && npm run dev # web :3000
手机应用也必须指向同一台 Go 服务器(在模拟器上需设置
SMARTID_REQUIRE_ATTESTATION=false)。
5. rpclient.ts —— 它如何与 RP-API 通信¶
web/src/lib/rpclient.ts 是 Java 版 GeregeSmartIdRpClient + WebDemo 的移植,
仅在服务端运行(由 route handler 调用)。
- 基础 URL ——
apiBase()=RP_API_BASE(或NEXT_PUBLIC_API_BASE)+/v3, 并去除末尾的/。 - 认证 —— 若设置了
RP_API_SECRET,authHeaders()返回Authorization: Bearer <secret>;否则返回空头(dev)。每个post()/get()都会附加该请求头。 - 请求体 ——
relyingPartyUUID、relyingPartyName、certificateLevel(QUALIFIED)、signatureProtocol(ACSP_V2)、interactions, 并按需附加rpChallenge/digest+hashType/initialCallbackUrl/onBehalfOf。 - 超时 —— 普通调用 15 秒,长轮询 140 秒(
AbortSignal.timeout)。 - PDF 加盖签章 ——
stampSignedPdf()针对已完成的 SIGN 会话,将原始 PDF 以application/pdf发送至POST /v3/signature/stamp/{sessionId},并接收加盖签章后的 PDF。
web/src/lib/x509subject.ts 是一个极简的 ASN.1 DER 解析器,用于从证书 subject 中
提取姓名与登记号(serialNumber)(用以替代 Java 中的 BouncyCastle)。
6. 链接¶
- RP 集成通用指南:RP 集成
- 使用同一套
/api/*的桌面客户端:macOS desktop - 公民标识号码:IDENTIFIERS.md
源码(GitHub)¶
- Web 演示:
https://github.com/gerege-systems/eid-platform-mn/tree/main/web - RP-API 客户端:
https://github.com/gerege-systems/eid-platform-mn/blob/main/web/src/lib/rpclient.ts - Route handlers:
https://github.com/gerege-systems/eid-platform-mn/tree/main/web/src/app/api /api/start:https://github.com/gerege-systems/eid-platform-mn/blob/main/web/src/app/api/start/route.ts/api/status:https://github.com/gerege-systems/eid-platform-mn/blob/main/web/src/app/api/status/route.ts/api/sign-pdf-download:https://github.com/gerege-systems/eid-platform-mn/blob/main/web/src/app/api/sign-pdf-download/route.ts
Next.js 16。 本仓库使用 Next.js 16(含破坏性变更)。在编写 Next.js 代码前, 请遵循
web/AGENTS.md中的说明,并查阅node_modules/next/dist/docs/, 而不要依赖训练数据。