中国电子口岸 UKey 代理服务器
一个高性能的 Go 代理服务器,通过 WebSocket 将 HTTP/JSON-RPC 请求桥接到中国电子口岸 UKey 硬件,提供身份验证、请求跟踪和 Webhook 通知功能。
功能特性
- 持久 WebSocket 连接:与 UKey 硬件保持稳定连接,支持自动重连
- 请求身份验证:基于 MD5 的签名验证机制
- 请求跟踪:使用 UUID v4 进行完整的请求/响应追踪
- Webhook 通知:
- UKey 故障时发送失败通知
- 记录所有请求和响应的审计日志
- 健康监控:每 30 秒自动进行健康检查
- 密码管理:从配置中自动注入密码
- 详细日志:带密码屏蔽的详细调试日志
安装
前置条件
- Go 1.21 或更高版本
- 可通过 WebSocket 访问的 UKey 硬件
构建
使用 Make:
bash
make build # 构建二进制文件
make build-all # 为所有平台构建(Linux、Windows、macOS)
make install # 安装到 $GOPATH/bin
手动构建:
bash
go mod download
go build -o chinaport-proxy
配置
创建 config.json
文件:
json
{
"server": {
"port": "8780"
},
"ukey": {
"url": "ws://127.0.0.1:61232",
"password": "88888888"
},
"auth": {
"app_secret": "your-secret-key"
},
"webhook": {
"failure_url": "https://your-webhook.com/failure",
"audit_url": "https://your-webhook.com/audit"
},
"log": {
"verbose": false
}
}
配置选项
字段 | 描述 | 默认值 |
---|---|---|
server.port | HTTP 服务器端口 | 8780 |
ukey.url | UKey WebSocket URL | ws://127.0.0.1:61232 |
ukey.password | UKey 密码(自动注入) | 88888888 |
auth.app_secret | 签名验证密钥(空=禁用) | - |
webhook.failure_url | 失败通知 URL | - |
webhook.audit_url | 审计日志 URL | - |
log.verbose | 启用调试日志 | false |
使用方法
启动服务器
使用 Make:
bash
make run # 使用默认 config.json 构建并运行
CONFIG=prod.json make run-config # 使用自定义配置文件构建并运行
make dev # 使用竞态检测器运行(开发模式)
直接执行:
bash
# 使用默认 config.json
./chinaport-proxy
# 使用自定义配置文件
./chinaport-proxy -config /path/to/config.json
API 端点
服务器暴露单个 RPC 端点:
POST /rpc
请求格式
无参数方法:
json
{
"_method": "cus-sec_SpcGetCertNo"
}
有参数方法:
json
{
"_method": "cus-sec_SpcSignDataAsPEM",
"args": {
"inData": "Hello World"
}
}
注意:
passwd
字段会从配置中自动注入,客户端不应发送- 对于不需要参数的方法,可以省略
args
字段
响应格式
成功响应:
json
{
"_method": "cus-sec_SpcGetCertNo",
"_status": "00",
"_args": {
"Result": true,
"Data": ["0177f045"],
"Error": []
}
}
错误响应(UKey 操作失败):
json
{
"_method": "cus-sec_SpcSignDataAsPEM",
"_status": "00",
"_args": {
"Result": false,
"Data": [],
"Error": ["参数检查失败,传入的args中必须包含必要的非空白参数:inData", "Err:Base50000"]
}
}
注意:_status
字段表示协议状态("00" = 成功),而 _args.Result
表示 UKey 操作是否成功。
支持的方法
cus-sec_SpcGetCertNo
- 获取证书编号(无需参数)cus-sec_SpcGetEntName
- 获取企业名称(无需参数)cus-sec_SpcGetEnvCertAsPEM
- 获取信封证书为 PEM 格式cus-sec_SpcGetSignCertAsPEM
- 获取签名证书为 PEM 格式cus-sec_SpcSHA1DigestAsPEM
- 计算 SHA1 摘要(需要szInfo
)cus-sec_SpcSignDataAsPEM
- 带哈希签名数据(需要inData
)cus-sec_SpcSignDataNoHashAsPEM
- 不带哈希签名数据(需要inData
)
示例 API 调用
获取企业名称:
bash
curl -H "Content-Type: application/json" \
-d '{"_method":"cus-sec_SpcGetEntName"}' \
http://127.0.0.1:8780/rpc
获取证书编号:
bash
curl -H "Content-Type: application/json" \
-d '{"_method":"cus-sec_SpcGetCertNo"}' \
http://127.0.0.1:8780/rpc
签名数据:
bash
curl -H "Content-Type: application/json" \
-d '{"_method":"cus-sec_SpcSignDataAsPEM","args":{"inData":"Hello World"}}' \
http://127.0.0.1:8780/rpc
身份验证
当配置了 auth.app_secret
时,所有请求必须包含身份验证头:
必需头信息
X-Timestamp
:当前时间戳(毫秒)X-Signature
:请求的 MD5 签名
签名算法
signature = MD5(METHOD + PATH + TIMESTAMP + BODY + APP_SECRET)
其中:
METHOD
:HTTP 方法(大写,例如 "POST")PATH
:请求路径(例如 "/rpc")TIMESTAMP
:与 X-Timestamp 头相同BODY
:原始请求体APP_SECRET
:配置的密钥
时间戳验证
请求必须在服务器时间的 3 秒内,以防止重放攻击。
示例(Node.js)
javascript
const crypto = require('crypto');
function makeAuthenticatedRequest(body) {
const timestamp = Date.now();
const method = 'POST';
const path = '/rpc';
const appSecret = 'your-secret-key';
const plaintext = method + path + timestamp + JSON.stringify(body) + appSecret;
const signature = crypto.createHash('md5').update(plaintext).digest('hex');
return fetch('http://localhost:8780/rpc', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Timestamp': timestamp.toString(),
'X-Signature': signature
},
body: JSON.stringify(body)
});
}
请求跟踪
每个请求都会分配一个 UUID v4 请求 ID 用于追踪:
- 请求 ID 在
X-Request-ID
响应头中返回 - 所有与请求相关的日志都包含此 ID
- 请求 ID 包含在 Webhook 通知中
Webhooks
失败 Webhook
当 UKey 操作失败或连接问题时触发。
负载:
json
{
"event": "ukey_malfunction",
"timestamp": 1723809415000,
"error": "错误描述",
"details": {
"type": "operation_failure",
"method": "cus-sec_SpcGetCertNo",
"errors": ["error1", "error2"]
}
}
失败类型:
operation_failure
:UKey 方法执行失败connection_failure
:WebSocket 连接丢失health_check_failure
:健康检查失败
审计 Webhook
为所有请求(成功或失败)触发,用于审计目的。
负载:
json
{
"event": "api_audit",
"timestamp": 1723809415000,
"request_id": "uuid-here",
"method": "cus-sec_SpcGetCertNo",
"request": {},
"response": {},
"success": true,
"error": "",
"duration_ms": 125
}
Webhook 身份验证
Webhooks 使用与传入请求相同的认证机制:
X-Timestamp
头包含当前时间戳X-Signature
头包含 MD5 签名
验证 Webhook 签名
要验证 Webhook 是否来自代理服务器,请实现相同的签名验证:
javascript
// Node.js 示例
const crypto = require('crypto');
function verifyWebhookSignature(req, appSecret) {
const timestamp = req.headers['x-timestamp'];
const signature = req.headers['x-signature'];
const method = req.method;
const path = req.path; // 例如 '/webhook/audit'
const body = req.rawBody; // 原始请求体字符串
// 检查时间戳(3 秒内)
const now = Date.now();
const timeDiff = Math.abs(now - parseInt(timestamp));
if (timeDiff > 3000) {
return false; // 时间戳过期
}
// 计算预期签名
const plaintext = method.toUpperCase() + path + timestamp + body + appSecret;
const expectedSignature = crypto.createHash('md5').update(plaintext).digest('hex');
return expectedSignature === signature;
}
// Express.js 中间件示例
app.use(express.json({
verify: (req, res, buf) => {
req.rawBody = buf.toString('utf8');
}
}));
app.post('/webhook/audit', (req, res) => {
if (!verifyWebhookSignature(req, 'your-secret-key')) {
return res.status(401).json({ error: '无效签名' });
}
// 处理 Webhook
console.log('审计事件:', req.body);
res.json({ status: '已接收' });
});
健康监控
服务器执行自动健康检查:
- 间隔:30 秒
- 方法:调用
cus-sec_SpcGetCertNo
验证 UKey 连接 - 失败时:触发重连并发送失败 Webhook
日志
日志级别
- 正常模式:INFO 级别及以上
- 详细模式:DEBUG 级别,包含详细的请求/响应日志
密码屏蔽
日志中的所有密码都会自动屏蔽为 ***MASKED***
以确保安全。
日志示例
2024/01/15 10:30:00 INFO 启动中国电子口岸代理服务器 address=:8780
2024/01/15 10:30:01 INFO WebSocket 已连接
2024/01/15 10:30:15 INFO 收到 RPC 请求 method=cus-sec_SpcGetCertNo request_id=abc-123
2024/01/15 10:30:15 INFO 请求成功完成 method=cus-sec_SpcGetCertNo request_id=abc-123
错误处理
HTTP 状态码
200 OK
:UKey 操作成功400 Bad Request
:UKey 操作失败或请求格式无效401 Unauthorized
:身份验证失败503 Service Unavailable
:UKey 连接问题
自动重连
- 指数退避,从 1 秒开始
- 最大退避:8 秒
- 无限重试直到连接恢复
信号处理
服务器处理 SIGTERM 和 SIGINT 信号:
- 停止接受新请求
- 等待活动请求完成(最多 10 秒)
- 关闭 WebSocket 连接
- 干净退出
开发
项目结构
chinaport-proxy/
├── main.go # 应用程序入口点
├── config.json # 配置文件
├── go.mod # Go 模块定义
├── config/
│ └── config.go # 配置结构
├── client/
│ └── ukey.go # WebSocket 客户端实现
├── handlers/
│ └── proxy.go # HTTP 请求处理器
├── services/
│ └── webhook.go # Webhook 服务
├── middleware/
│ └── auth.go # 身份验证中间件
├── auth/
│ └── signature.go # 签名生成/验证
├── protocol/
│ └── types.go # 协议类型定义
└── utils/
└── logging.go # 日志工具
代码质量
使用 Make:
bash
make fmt # 格式化代码
make vet # 检查代码
make check # 运行 fmt 和 vet
生产环境构建
使用 Make:
bash
make build-linux # Linux(amd64, arm64)
make build-windows # Windows(amd64)
make build-darwin # macOS(amd64, arm64)
make build-all # 所有平台
手动交叉编译:
bash
# Linux
GOOS=linux GOARCH=amd64 go build -o chinaport-proxy
# Windows
GOOS=windows GOARCH=amd64 go build -o chinaport-proxy.exe
其他 Make 命令
bash
make clean # 删除构建产物
make fmt # 格式化代码
make vet # 检查代码
make deps # 下载依赖
make tidy # 整理依赖