跳转至

Web RP demo —— 开发者指南

eID 平台的浏览器 RP 演示 —— 一个使用 Next.js(App Router、TypeScript)编写的 依赖方示例。它在浏览器中演示通过 eID Mongolia 手机应用完成 二维码 / РД 推送登录, 以及为 PDF 加盖合格电子签名的完整流程。源码:web/(对 Java 版 smartid-demoWebDemo.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/PhoneCrypto JVM 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:响应中包含 sessionIDvc.valueresult.endResultresult.documentNumbercert.value(base64 DER)、cert.certificateLevel

secret 与 RP 标识存放位置

位置 说明
RP_API_SECRET env,仅服务端 rpclient.tsauthHeaders()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.tsxdemo/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. 运行

cd web
npm install
npm run dev      # http://localhost:3000
npm run build    # 检查生产构建
npm run lint

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_BASENEXT_PUBLIC_API_BASE 均为空, 仅在 dev 环境会回退到 http://localhost:8080/v3rpclient.tsapiBase())。 因此本地测试只需在 :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_SECRETauthHeaders() 返回 Authorization: Bearer <secret>;否则返回空头(dev)。每个 post()/get() 都会附加该请求头。
  • 请求体 —— relyingPartyUUIDrelyingPartyNamecertificateLevelQUALIFIED)、signatureProtocolACSP_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. 链接

源码(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/starthttps://github.com/gerege-systems/eid-platform-mn/blob/main/web/src/app/api/start/route.ts
  • /api/statushttps://github.com/gerege-systems/eid-platform-mn/blob/main/web/src/app/api/status/route.ts
  • /api/sign-pdf-downloadhttps://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/, 而不要依赖训练数据。