Parcourir la source

Add initial project structure and Docker support for SM2 signature service

- Created project files including .classpath, .project, and .dockerignore.
- Added Dockerfile and docker-compose.yml for containerized deployment.
- Implemented deploy.sh script for easy deployment.
- Introduced core Java classes for SM2 signature functionality.
- Added comprehensive documentation in Chinese for deployment and API usage.
- Included necessary libraries in the lib directory for project dependencies.
light il y a 2 semaines
commit
63c81116f5
87 fichiers modifiés avec 5444 ajouts et 0 suppressions
  1. BIN
      .DS_Store
  2. 37 0
      .classpath
  3. 7 0
      .dockerignore
  4. 28 0
      .project
  5. 4 0
      .settings/org.eclipse.core.resources.prefs
  6. 2 0
      .settings/org.eclipse.core.runtime.prefs
  7. 15 0
      .settings/org.eclipse.jdt.core.prefs
  8. 277 0
      API使用说明.md
  9. 24 0
      Dockerfile
  10. 278 0
      README.md
  11. BIN
      bin/.DS_Store
  12. BIN
      bin/demo/.DS_Store
  13. BIN
      bin/demo/com/GenerateKeyPair.class
  14. BIN
      bin/demo/com/SignServer$HealthHandler.class
  15. BIN
      bin/demo/com/SignServer$SignHandler.class
  16. BIN
      bin/demo/com/SignServer$VerifyHandler.class
  17. BIN
      bin/demo/com/SignServer.class
  18. BIN
      bin/demo/com/Test.class
  19. BIN
      bin/demo/com/entity/AgreementTxnRequest.class
  20. BIN
      bin/demo/com/entity/AgreementTxnResponse.class
  21. BIN
      bin/demo/com/service/IAgreementTxnController.class
  22. BIN
      bin/demo/com/util/ByteArrayUtil.class
  23. BIN
      bin/demo/com/util/cxf/CxfService$1.class
  24. BIN
      bin/demo/com/util/cxf/CxfService$2.class
  25. BIN
      bin/demo/com/util/cxf/CxfService.class
  26. BIN
      bin/demo/com/util/sm/Cipher.class
  27. BIN
      bin/demo/com/util/sm/SM2.class
  28. BIN
      bin/demo/com/util/sm/SM2Utils.class
  29. BIN
      bin/demo/com/util/sm/SM3.class
  30. BIN
      bin/demo/com/util/sm/SM3Digest.class
  31. BIN
      bin/demo/com/util/sm/SM3Utils.class
  32. BIN
      bin/demo/com/util/sm/Utils.class
  33. 85 0
      deploy.sh
  34. 19 0
      docker-compose.yml
  35. BIN
      lib/XmlSchema-1.4.4.jar
  36. BIN
      lib/bcprov-jdk16-1.46.jar
  37. BIN
      lib/commons-lang-2.4.jar
  38. BIN
      lib/commons-logging-1.1.3.jar
  39. BIN
      lib/cxf-api-2.2.3.jar
  40. BIN
      lib/cxf-common-schemas-2.2.3.jar
  41. BIN
      lib/cxf-common-utilities-2.2.3.jar
  42. BIN
      lib/cxf-rt-bindings-soap-2.2.3.jar
  43. BIN
      lib/cxf-rt-bindings-xml-2.2.3.jar
  44. BIN
      lib/cxf-rt-core-2.2.3.jar
  45. BIN
      lib/cxf-rt-databinding-jaxb-2.2.3.jar
  46. BIN
      lib/cxf-rt-frontend-jaxws-2.2.3.jar
  47. BIN
      lib/cxf-rt-frontend-simple-2.2.3.jar
  48. BIN
      lib/cxf-rt-transports-http-2.2.3.jar
  49. BIN
      lib/cxf-rt-ws-addr-2.2.3.jar
  50. BIN
      lib/cxf-rt-ws-security-2.2.3.jar
  51. BIN
      lib/cxf-tools-common-2.2.3.jar
  52. BIN
      lib/fastjson-1.1.41.jar
  53. BIN
      lib/fastjson-1.2.40.jar
  54. BIN
      lib/httpclient-4.0.1.jar
  55. BIN
      lib/httpcore-4.0.1.jar
  56. BIN
      lib/log4j-1.2.14.jar
  57. BIN
      lib/log4j-api-2.5.jar
  58. BIN
      lib/slf4j-api-1.5.6.jar
  59. BIN
      lib/slf4j-log4j12-1.5.6.jar
  60. BIN
      lib/wsdl4j-1.6.2.jar
  61. BIN
      lib/wsdl4j-1.6.3.jar
  62. BIN
      lib/wss4j-1.5.6.jar
  63. BIN
      lib/wss4j-1.5.8.jar
  64. BIN
      lib/xml-resolver-1.2.jar
  65. BIN
      lib/xmlsec-1.3.0.jar
  66. BIN
      src/.DS_Store
  67. BIN
      src/demo/.DS_Store
  68. 100 0
      src/demo/com/GenerateKeyPair.java
  69. 394 0
      src/demo/com/SignServer.java
  70. 145 0
      src/demo/com/Test.java
  71. 385 0
      src/demo/com/entity/AgreementTxnRequest.java
  72. 278 0
      src/demo/com/entity/AgreementTxnResponse.java
  73. 20 0
      src/demo/com/service/IAgreementTxnController.java
  74. 150 0
      src/demo/com/util/ByteArrayUtil.java
  75. 90 0
      src/demo/com/util/cxf/CxfService.java
  76. 94 0
      src/demo/com/util/sm/Cipher.java
  77. 157 0
      src/demo/com/util/sm/SM2.java
  78. 198 0
      src/demo/com/util/sm/SM2Utils.java
  79. 255 0
      src/demo/com/util/sm/SM3.java
  80. 121 0
      src/demo/com/util/sm/SM3Digest.java
  81. 45 0
      src/demo/com/util/sm/SM3Utils.java
  82. 612 0
      src/demo/com/util/sm/Utils.java
  83. 80 0
      快速部署.txt
  84. 496 0
      故障排除.md
  85. 320 0
      私钥配置说明.md
  86. 281 0
      私钥错误修复说明.md
  87. 447 0
      部署说明.md

BIN
.DS_Store


+ 37 - 0
.classpath

@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+	<classpathentry kind="src" path="src"/>
+	<classpathentry kind="lib" path="lib/bcprov-jdk16-1.46.jar"/>
+	<classpathentry kind="lib" path="lib/commons-lang-2.4.jar"/>
+	<classpathentry kind="lib" path="lib/commons-logging-1.1.3.jar"/>
+	<classpathentry kind="lib" path="lib/cxf-api-2.2.3.jar"/>
+	<classpathentry kind="lib" path="lib/cxf-common-schemas-2.2.3.jar"/>
+	<classpathentry kind="lib" path="lib/cxf-common-utilities-2.2.3.jar"/>
+	<classpathentry kind="lib" path="lib/cxf-rt-bindings-soap-2.2.3.jar"/>
+	<classpathentry kind="lib" path="lib/cxf-rt-bindings-xml-2.2.3.jar"/>
+	<classpathentry kind="lib" path="lib/cxf-rt-core-2.2.3.jar"/>
+	<classpathentry kind="lib" path="lib/cxf-rt-databinding-jaxb-2.2.3.jar"/>
+	<classpathentry kind="lib" path="lib/cxf-rt-frontend-jaxws-2.2.3.jar"/>
+	<classpathentry kind="lib" path="lib/cxf-rt-frontend-simple-2.2.3.jar"/>
+	<classpathentry kind="lib" path="lib/cxf-rt-transports-http-2.2.3.jar"/>
+	<classpathentry kind="lib" path="lib/cxf-rt-ws-addr-2.2.3.jar"/>
+	<classpathentry kind="lib" path="lib/cxf-rt-ws-security-2.2.3.jar"/>
+	<classpathentry kind="lib" path="lib/cxf-tools-common-2.2.3.jar"/>
+	<classpathentry kind="lib" path="lib/fastjson-1.1.41.jar"/>
+	<classpathentry kind="lib" path="lib/fastjson-1.2.40.jar"/>
+	<classpathentry kind="lib" path="lib/httpclient-4.0.1.jar"/>
+	<classpathentry kind="lib" path="lib/httpcore-4.0.1.jar"/>
+	<classpathentry kind="lib" path="lib/log4j-1.2.14.jar"/>
+	<classpathentry kind="lib" path="lib/log4j-api-2.5.jar"/>
+	<classpathentry kind="lib" path="lib/slf4j-api-1.5.6.jar"/>
+	<classpathentry kind="lib" path="lib/slf4j-log4j12-1.5.6.jar"/>
+	<classpathentry kind="lib" path="lib/wsdl4j-1.6.2.jar"/>
+	<classpathentry kind="lib" path="lib/wsdl4j-1.6.3.jar"/>
+	<classpathentry kind="lib" path="lib/wss4j-1.5.6.jar"/>
+	<classpathentry kind="lib" path="lib/wss4j-1.5.8.jar"/>
+	<classpathentry kind="lib" path="lib/xml-resolver-1.2.jar"/>
+	<classpathentry kind="lib" path="lib/XmlSchema-1.4.4.jar"/>
+	<classpathentry kind="lib" path="lib/xmlsec-1.3.0.jar"/>
+	<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
+	<classpathentry kind="output" path="bin"/>
+</classpath>

+ 7 - 0
.dockerignore

@@ -0,0 +1,7 @@
+src/
+.git/
+.gitignore
+*.md
+Dockerfile
+.dockerignore
+

+ 28 - 0
.project

@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+	<name>demo</name>
+	<comment></comment>
+	<projects>
+	</projects>
+	<buildSpec>
+		<buildCommand>
+			<name>org.eclipse.jdt.core.javabuilder</name>
+			<arguments>
+			</arguments>
+		</buildCommand>
+	</buildSpec>
+	<natures>
+		<nature>org.eclipse.jdt.core.javanature</nature>
+	</natures>
+	<filteredResources>
+		<filter>
+			<id>1761270179005</id>
+			<name></name>
+			<type>30</type>
+			<matcher>
+				<id>org.eclipse.core.resources.regexFilterMatcher</id>
+				<arguments>node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__</arguments>
+			</matcher>
+		</filter>
+	</filteredResources>
+</projectDescription>

+ 4 - 0
.settings/org.eclipse.core.resources.prefs

@@ -0,0 +1,4 @@
+eclipse.preferences.version=1
+encoding//src/demo/com/entity/AgreementTxnRequest.java=UTF-8
+encoding//src/demo/com/entity/AgreementTxnResponse.java=UTF-8
+encoding/<project>=UTF-8

+ 2 - 0
.settings/org.eclipse.core.runtime.prefs

@@ -0,0 +1,2 @@
+eclipse.preferences.version=1
+line.separator=\n

+ 15 - 0
.settings/org.eclipse.jdt.core.prefs

@@ -0,0 +1,15 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
+org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8
+org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
+org.eclipse.jdt.core.compiler.compliance=1.8
+org.eclipse.jdt.core.compiler.debug.lineNumber=generate
+org.eclipse.jdt.core.compiler.debug.localVariable=generate
+org.eclipse.jdt.core.compiler.debug.sourceFile=generate
+org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
+org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled
+org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
+org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning
+org.eclipse.jdt.core.compiler.release=disabled
+org.eclipse.jdt.core.compiler.source=1.8

+ 277 - 0
API使用说明.md

@@ -0,0 +1,277 @@
+# 签名服务API使用说明
+
+## 服务介绍
+
+这是一个基于SM2算法的HTTP签名服务,提供数据签名和验签功能。
+
+## 启动服务
+
+```bash
+cd /Users/light/www/pros/xingfutong-java
+java -cp "bin:lib/*" demo.com.SignServer
+```
+
+服务启动后将监听 **8888端口**
+
+## API接口说明
+
+### 1. 健康检查接口
+
+**接口地址**: `GET http://localhost:8888/api/health`
+
+**功能**: 检查服务是否正常运行
+
+**响应示例**:
+```json
+{
+    "status": "ok",
+    "timestamp": 1761288550040
+}
+```
+
+---
+
+### 2. 签名接口
+
+**接口地址**: `POST http://localhost:8888/api/sign`
+
+**功能**: 对传入的数据进行SM2签名
+
+**请求头**:
+```
+Content-Type: application/json
+```
+
+**请求参数**:
+```json
+{
+    "data": {
+        "key1": "value1",
+        "key2": "value2",
+        ...
+    },
+    "priKey": "您的SM2私钥(可选)",
+    "reqOrgNo": "机构号(可选)"
+}
+```
+
+**参数说明**:
+- `data`: 对象类型,必填,包含需要签名的所有键值对
+- `priKey`: 字符串类型,可选,SM2私钥(十六进制格式)。如不提供则使用服务器配置的默认私钥
+- `reqOrgNo`: 字符串类型,可选,请求机构号。如不提供则使用服务器配置的默认机构号
+- 系统会自动将key转为大写,并按字母顺序排序
+- 空值字段会被自动过滤
+
+**响应示例**:
+```json
+{
+    "success": true,
+    "signData": "KEY1=value1|KEY2=value2",
+    "sign": "304402206FC7DE55665BE3C062355D88A9A77AE5877FE3D5B42EBB3EAA46AF50E08DB53A0220105E6C4399B50825E98D6816FBF9B6D2E4603EB5EB6F5440F60728EDD8D0BA46",
+    "timestamp": 1761288550594
+}
+```
+
+**响应字段说明**:
+- `success`: 是否成功
+- `signData`: 实际签名的数据串(按KEY升序排列)
+- `sign`: 十六进制签名结果
+- `timestamp`: 时间戳
+
+**使用示例1 - 使用默认私钥**:
+```bash
+curl -X POST http://localhost:8888/api/sign \
+  -H "Content-Type: application/json" \
+  -d '{
+    "data": {
+      "version": "1.0",
+      "txnType": "20250",
+      "reqOrgId": "201811200001003",
+      "cardNo": "9206068000000010425"
+    }
+  }'
+```
+
+**使用示例2 - 传入自定义私钥**:
+```bash
+curl -X POST http://localhost:8888/api/sign \
+  -H "Content-Type: application/json" \
+  -d '{
+    "data": {
+      "version": "1.0",
+      "txnType": "20250",
+      "reqOrgId": "201811200001003"
+    },
+    "priKey": "您的SM2私钥(64位十六进制字符串)",
+    "reqOrgNo": "您的机构号"
+  }'
+```
+
+---
+
+### 3. 验签接口
+
+**接口地址**: `POST http://localhost:8888/api/verify`
+
+**功能**: 验证签名是否正确
+
+**请求头**:
+```
+Content-Type: application/json
+```
+
+**请求参数**:
+```json
+{
+    "data": {
+        "key1": "value1",
+        "key2": "value2",
+        ...
+    },
+    "sign": "签名字符串",
+    "pubKey": "公钥(可选)",
+    "reqOrgNo": "机构号(可选)"
+}
+```
+
+**参数说明**:
+- `data`: 对象类型,必填,包含需要验签的所有键值对
+- `sign`: 字符串类型,必填,待验证的签名(十六进制格式)
+- `pubKey`: 字符串类型,可选,SM2公钥(十六进制格式)。如不提供则使用服务器配置的默认公钥
+- `reqOrgNo`: 字符串类型,可选,请求机构号。如不提供则使用服务器配置的默认机构号
+
+**响应示例**:
+```json
+{
+    "success": true,
+    "valid": true,
+    "signData": "KEY1=value1|KEY2=value2",
+    "timestamp": 1761288550803
+}
+```
+
+**响应字段说明**:
+- `success`: 请求是否成功
+- `valid`: 签名是否有效
+- `signData`: 实际验签的数据串
+- `timestamp`: 时间戳
+
+**使用示例1 - 使用默认公钥**:
+```bash
+curl -X POST http://localhost:8888/api/verify \
+  -H "Content-Type: application/json" \
+  -d '{
+    "data": {
+      "version": "1.0",
+      "txnType": "20250",
+      "reqOrgId": "201811200001003"
+    },
+    "sign": "304402206FC7DE55665BE3C062355D88A9A77AE5877FE3D5B42EBB3EAA46AF50E08DB53A..."
+  }'
+```
+
+**使用示例2 - 传入自定义公钥**:
+```bash
+curl -X POST http://localhost:8888/api/verify \
+  -H "Content-Type: application/json" \
+  -d '{
+    "data": {
+      "version": "1.0",
+      "txnType": "20250",
+      "reqOrgId": "201811200001003"
+    },
+    "sign": "304402206FC7DE55665BE3C062355D88A9A77AE5877FE3D5B42EBB3EAA46AF50E08DB53A...",
+    "pubKey": "您的SM2公钥(130位十六进制字符串)",
+    "reqOrgNo": "您的机构号"
+  }'
+```
+
+---
+
+## 错误响应
+
+当发生错误时,返回格式如下:
+
+```json
+{
+    "success": false,
+    "error": "错误信息",
+    "timestamp": 1761288550848
+}
+```
+
+**常见错误**:
+- `data参数不能为空` - 签名/验签时data对象为空
+- `data和sign参数不能为空` - 验签时缺少必要参数
+- `只支持POST请求` - 使用了错误的HTTP方法
+
+---
+
+## 快速测试
+
+项目提供了测试脚本,可以快速测试所有接口:
+
+```bash
+./test_sign_api.sh
+```
+
+测试脚本会自动执行以下测试:
+1. 健康检查
+2. 简单参数签名
+3. 完整参数签名
+4. 签名验证
+5. 错误处理测试
+
+---
+
+## 签名规则说明
+
+1. **字段处理**:
+   - 所有key会转换为大写
+   - 空值字段会被过滤
+   - 按key的字母顺序升序排列
+
+2. **签名串生成**:
+   - 格式: `KEY1=value1|KEY2=value2|KEY3=value3`
+   - 使用竖线(|)分隔各字段
+   - 使用等号(=)连接key和value
+
+3. **签名算法**:
+   - 使用SM2国密算法
+   - 编码方式: UTF-8
+   - 输出格式: 十六进制字符串
+
+---
+
+## 注意事项
+
+1. 服务默认监听8888端口,如端口被占用请修改 `SignServer.java` 中的端口配置
+2. 签名使用的私钥和机构号已在代码中配置
+3. 验签使用的是平台公钥,与签名私钥不是配对的,所以验签测试会返回false(这是正常现象)
+4. 如需在生产环境使用,请修改配置并确保密钥安全
+
+---
+
+## 配置说明
+
+在 `SignServer.java` 中可以配置:
+
+```java
+// 请求机构号
+private static String reqOrgNo = "201811200001003";
+// 请求机构的私钥
+private static String priKey = "3164EE0DF2BCA7A12309383E3305DD6563A28DFE53F65BBD60B3A1D7F80AC275";
+// 平台给商户的公钥
+private static String sltPubKey = "046875695CDF1EF046ABB231FDAFA6DCA2AF1E5719EAC00DE80D65FEF03F8485DC9DCBBC10A9A46D565B4CDCEE3510F276209657CAE5BAC10C9678583A44F7F100";
+```
+
+---
+
+## 联系支持
+
+如有问题,请检查:
+1. 服务是否正常启动
+2. 端口是否被占用
+3. 请求格式是否正确
+4. 参数是否完整
+

+ 24 - 0
Dockerfile

@@ -0,0 +1,24 @@
+# 使用OpenJDK 8作为基础镜像
+FROM openjdk:8-jre-alpine
+
+# 设置工作目录
+WORKDIR /app
+
+# 复制lib目录和编译好的class文件
+COPY lib/ /app/lib/
+COPY bin/ /app/bin/
+
+# 可选:如果需要配置文件
+# COPY config.properties /app/config.properties
+
+# 暴露端口
+EXPOSE 8888
+
+# 设置环境变量(可选,也可以在运行时通过-e参数传入)
+# ENV SM2_PRIVATE_KEY=""
+# ENV SLT_PUBLIC_KEY=""
+# ENV REQ_ORG_NO=""
+
+# 启动命令
+CMD ["java", "-cp", "bin:lib/*", "demo.com.SignServer"]
+

+ 278 - 0
README.md

@@ -0,0 +1,278 @@
+# SM2签名服务 - HTTP API
+
+基于国密SM2算法的签名和验签HTTP服务,使用纯Java实现(BouncyCastle),支持Docker部署。
+
+## 🚀 快速开始
+
+### 本地运行(需要Java)
+
+```bash
+# 编译
+javac -encoding UTF-8 -d bin -cp "lib/*" $(find src -name "*.java")
+
+# 运行
+java -cp "bin:lib/*" demo.com.SignServer
+```
+
+访问: http://localhost:8888
+
+### Docker部署(无需Java)⭐
+
+```bash
+# 一键部署
+./deploy.sh
+
+# 或使用docker-compose
+docker-compose up -d
+```
+
+**服务器只需要Docker,不需要Java环境!**
+
+## 📡 API接口
+
+### 1. 签名接口
+```bash
+POST http://localhost:8888/api/sign
+
+# 请求体
+{
+  "data": {
+    "version": "1.0",
+    "txnType": "20250"
+  },
+  "priKey": "私钥(可选)",
+  "reqOrgNo": "机构号(可选)"
+}
+
+# 响应
+{
+  "success": true,
+  "signData": "TXNTYPE=20250|VERSION=1.0",
+  "sign": "304402206FC7...",
+  "timestamp": 1761288550594
+}
+```
+
+### 2. 验签接口
+```bash
+POST http://localhost:8888/api/verify
+
+# 请求体
+{
+  "data": {
+    "version": "1.0"
+  },
+  "sign": "304402206FC7...",
+  "pubKey": "公钥(可选)",
+  "reqOrgNo": "机构号(可选)"
+}
+
+# 响应
+{
+  "success": true,
+  "valid": true,
+  "signData": "VERSION=1.0",
+  "timestamp": 1761288550803
+}
+```
+
+### 3. 健康检查
+```bash
+GET http://localhost:8888/api/health
+
+# 响应
+{
+  "status": "ok",
+  "timestamp": 1761288550040
+}
+```
+
+## 🔑 私钥配置方式
+
+支持5种配置方式(按优先级从高到低):
+
+1. **API接口传入**(最灵活)⭐
+   - 在请求中传入 `priKey` 和 `reqOrgNo`
+   - 适合多租户场景
+
+2. **命令行参数**
+   ```bash
+   java -cp "bin:lib/*" demo.com.SignServer "" "机构号" "私钥" "公钥"
+   ```
+
+3. **环境变量**(推荐生产环境)⭐
+   ```bash
+   export SM2_PRIVATE_KEY="私钥"
+   export REQ_ORG_NO="机构号"
+   java -cp "bin:lib/*" demo.com.SignServer
+   ```
+
+4. **配置文件**
+   ```properties
+   # config.properties
+   reqOrgNo=201811200001003
+   priKey=您的私钥
+   sltPubKey=您的公钥
+   ```
+
+5. **代码默认值**(仅供测试)
+
+详细说明请查看: [私钥配置说明.md](私钥配置说明.md)
+
+## 📦 技术栈
+
+- **语言**: Java 8+
+- **加密库**: BouncyCastle 1.46 (纯Java实现)
+- **算法**: 国密SM2/SM3
+- **HTTP服务器**: JDK内置HttpServer
+- **容器**: Docker
+
+**不依赖OpenSSL,不需要安装系统扩展!**
+
+## 📚 文档
+
+- [API使用说明.md](API使用说明.md) - 详细的API文档
+- [部署说明.md](部署说明.md) - Docker部署完整指南
+- [私钥配置说明.md](私钥配置说明.md) - 各种私钥配置方式
+- [快速部署.txt](快速部署.txt) - 快速参考卡片
+
+## 🐳 Docker部署
+
+### 使用docker-compose(推荐)
+
+```bash
+# 启动
+docker-compose up -d
+
+# 查看日志
+docker-compose logs -f
+
+# 停止
+docker-compose down
+```
+
+### 使用Dockerfile
+
+```bash
+# 构建镜像
+docker build -t sign-server:latest .
+
+# 运行容器
+docker run -d \
+  --name sm2-sign-server \
+  -p 8888:8888 \
+  --restart unless-stopped \
+  sign-server:latest
+```
+
+### 传入自定义密钥
+
+```bash
+docker run -d \
+  --name sm2-sign-server \
+  -p 8888:8888 \
+  -e SM2_PRIVATE_KEY="您的私钥" \
+  -e SLT_PUBLIC_KEY="您的公钥" \
+  -e REQ_ORG_NO="您的机构号" \
+  sign-server:latest
+```
+
+## 🔒 安全建议
+
+1. ✅ 生产环境使用HTTPS
+2. ✅ 不要将私钥提交到代码仓库
+3. ✅ 使用环境变量或配置文件管理密钥
+4. ✅ 配置文件设置严格权限 `chmod 600`
+5. ✅ API传入密钥时必须使用HTTPS
+
+## 📊 性能
+
+- 镜像大小: ~85MB (Alpine Linux)
+- 启动时间: ~3秒
+- 内存占用: ~100-200MB
+- 签名速度: ~100-500 TPS (取决于硬件)
+
+## 🛠️ 开发
+
+### 项目结构
+
+```
+xingfutong-java/
+├── src/demo/com/
+│   ├── SignServer.java          # HTTP签名服务器
+│   ├── Test.java                 # 测试类
+│   ├── entity/                   # 实体类
+│   ├── service/                  # 服务接口
+│   └── util/
+│       ├── ByteArrayUtil.java    # 字节工具
+│       ├── cxf/                  # CXF工具
+│       └── sm/                   # SM2/SM3实现
+│           ├── SM2.java          # SM2核心算法
+│           ├── SM2Utils.java     # SM2工具类
+│           ├── SM3.java          # SM3算法
+│           ├── Cipher.java       # 加密器
+│           └── Utils.java        # 工具类
+├── lib/                          # 依赖库
+│   ├── bcprov-jdk16-1.46.jar    # BouncyCastle
+│   ├── fastjson-1.2.40.jar      # JSON解析
+│   └── ...
+├── bin/                          # 编译输出
+├── Dockerfile                    # Docker镜像定义
+├── docker-compose.yml            # Docker编排
+├── deploy.sh                     # 一键部署脚本
+└── *.md                          # 文档
+```
+
+### 编译
+
+```bash
+javac -encoding UTF-8 -d bin -cp "lib/*" $(find src -name "*.java")
+```
+
+### 运行
+
+```bash
+java -cp "bin:lib/*" demo.com.SignServer
+```
+
+## ❓ 常见问题
+
+### Q: 服务器没有Java怎么办?
+**A**: 使用Docker部署,只需要安装Docker即可。
+
+### Q: 私钥存在哪里最安全?
+**A**: 多租户场景通过API传入;单租户使用环境变量或Secret管理工具。
+
+### Q: 支持什么版本的Java?
+**A**: Java 8及以上版本。Docker镜像使用OpenJDK 8。
+
+### Q: SM2是什么?
+**A**: 国密SM2是中国自主研发的椭圆曲线公钥密码算法,相当于国际上的ECDSA。
+
+### Q: 为什么不用OpenSSL?
+**A**: 使用纯Java的BouncyCastle实现,跨平台、易部署、无需安装系统扩展。
+
+## 📝 更新日志
+
+### v1.0.0
+- ✅ SM2签名和验签功能
+- ✅ HTTP API服务
+- ✅ 支持API传入私钥
+- ✅ 支持多种配置方式
+- ✅ Docker容器化部署
+- ✅ 完整文档
+
+## 📄 许可证
+
+本项目仅供学习和测试使用。
+
+## 🤝 贡献
+
+欢迎提交Issue和Pull Request。
+
+---
+
+**快速上手**: 查看 [快速部署.txt](快速部署.txt)
+
+**详细文档**: 查看 [部署说明.md](部署说明.md)
+

BIN
bin/.DS_Store


BIN
bin/demo/.DS_Store


BIN
bin/demo/com/GenerateKeyPair.class


BIN
bin/demo/com/SignServer$HealthHandler.class


BIN
bin/demo/com/SignServer$SignHandler.class


BIN
bin/demo/com/SignServer$VerifyHandler.class


BIN
bin/demo/com/SignServer.class


BIN
bin/demo/com/Test.class


BIN
bin/demo/com/entity/AgreementTxnRequest.class


BIN
bin/demo/com/entity/AgreementTxnResponse.class


BIN
bin/demo/com/service/IAgreementTxnController.class


BIN
bin/demo/com/util/ByteArrayUtil.class


BIN
bin/demo/com/util/cxf/CxfService$1.class


BIN
bin/demo/com/util/cxf/CxfService$2.class


BIN
bin/demo/com/util/cxf/CxfService.class


BIN
bin/demo/com/util/sm/Cipher.class


BIN
bin/demo/com/util/sm/SM2.class


BIN
bin/demo/com/util/sm/SM2Utils.class


BIN
bin/demo/com/util/sm/SM3.class


BIN
bin/demo/com/util/sm/SM3Digest.class


BIN
bin/demo/com/util/sm/SM3Utils.class


BIN
bin/demo/com/util/sm/Utils.class


+ 85 - 0
deploy.sh

@@ -0,0 +1,85 @@
+#!/bin/bash
+
+echo "=========================================="
+echo "SM2签名服务 - 一键部署脚本"
+echo "=========================================="
+echo ""
+
+# 检查Docker
+if ! command -v docker &> /dev/null; then
+    echo "❌ 错误: 未安装Docker"
+    echo ""
+    echo "请先安装Docker:"
+    echo "curl -fsSL https://get.docker.com | sh"
+    exit 1
+fi
+
+echo "✅ Docker已安装: $(docker --version)"
+echo ""
+
+# 停止旧容器
+if docker ps -a | grep -q sm2-sign-server; then
+    echo "停止旧容器..."
+    docker stop sm2-sign-server 2>/dev/null
+    docker rm sm2-sign-server 2>/dev/null
+fi
+
+# 构建镜像
+echo "构建Docker镜像..."
+docker build -t sign-server:latest .
+
+if [ $? -ne 0 ]; then
+    echo "❌ 镜像构建失败"
+    exit 1
+fi
+
+echo "✅ 镜像构建成功"
+echo ""
+
+# 启动容器
+echo "启动容器..."
+docker run -d \
+  --name sm2-sign-server \
+  -p 8888:8888 \
+  --restart unless-stopped \
+  --log-opt max-size=10m \
+  --log-opt max-file=3 \
+  sign-server:latest
+
+if [ $? -ne 0 ]; then
+    echo "❌ 容器启动失败"
+    exit 1
+fi
+
+echo "✅ 容器启动成功"
+echo ""
+
+# 等待服务启动
+echo "等待服务启动..."
+sleep 3
+
+# 健康检查
+echo "检查服务状态..."
+HEALTH_CHECK=$(curl -s http://localhost:8888/api/health)
+
+if [ $? -eq 0 ]; then
+    echo "✅ 服务运行正常"
+    echo "响应: $HEALTH_CHECK"
+else
+    echo "⚠️  健康检查失败,查看日志:"
+    docker logs --tail 20 sm2-sign-server
+fi
+
+echo ""
+echo "=========================================="
+echo "部署完成!"
+echo "=========================================="
+echo "访问地址: http://localhost:8888"
+echo ""
+echo "常用命令:"
+echo "  查看日志: docker logs -f sm2-sign-server"
+echo "  停止服务: docker stop sm2-sign-server"
+echo "  启动服务: docker start sm2-sign-server"
+echo "  重启服务: docker restart sm2-sign-server"
+echo "=========================================="
+

+ 19 - 0
docker-compose.yml

@@ -0,0 +1,19 @@
+version: '3.8'
+
+services:
+  sign-server:
+    build: .
+    image: sign-server:latest
+    container_name: sm2-sign-server
+    ports:
+      - "8888:8888"
+    # environment:
+      # 可选:通过环境变量配置密钥
+      # SM2_PRIVATE_KEY: "您的私钥"
+      # SLT_PUBLIC_KEY: "您的公钥"
+      # REQ_ORG_NO: "您的机构号"
+    restart: unless-stopped
+    # volumes:
+      # 如果需要配置文件,可以挂载
+      # - ./config.properties:/app/config.properties:ro
+

BIN
lib/XmlSchema-1.4.4.jar


BIN
lib/bcprov-jdk16-1.46.jar


BIN
lib/commons-lang-2.4.jar


BIN
lib/commons-logging-1.1.3.jar


BIN
lib/cxf-api-2.2.3.jar


BIN
lib/cxf-common-schemas-2.2.3.jar


BIN
lib/cxf-common-utilities-2.2.3.jar


BIN
lib/cxf-rt-bindings-soap-2.2.3.jar


BIN
lib/cxf-rt-bindings-xml-2.2.3.jar


BIN
lib/cxf-rt-core-2.2.3.jar


BIN
lib/cxf-rt-databinding-jaxb-2.2.3.jar


BIN
lib/cxf-rt-frontend-jaxws-2.2.3.jar


BIN
lib/cxf-rt-frontend-simple-2.2.3.jar


BIN
lib/cxf-rt-transports-http-2.2.3.jar


BIN
lib/cxf-rt-ws-addr-2.2.3.jar


BIN
lib/cxf-rt-ws-security-2.2.3.jar


BIN
lib/cxf-tools-common-2.2.3.jar


BIN
lib/fastjson-1.1.41.jar


BIN
lib/fastjson-1.2.40.jar


BIN
lib/httpclient-4.0.1.jar


BIN
lib/httpcore-4.0.1.jar


BIN
lib/log4j-1.2.14.jar


BIN
lib/log4j-api-2.5.jar


BIN
lib/slf4j-api-1.5.6.jar


BIN
lib/slf4j-log4j12-1.5.6.jar


BIN
lib/wsdl4j-1.6.2.jar


BIN
lib/wsdl4j-1.6.3.jar


BIN
lib/wss4j-1.5.6.jar


BIN
lib/wss4j-1.5.8.jar


BIN
lib/xml-resolver-1.2.jar


BIN
lib/xmlsec-1.3.0.jar


BIN
src/.DS_Store


BIN
src/demo/.DS_Store


+ 100 - 0
src/demo/com/GenerateKeyPair.java

@@ -0,0 +1,100 @@
+package demo.com;
+
+import java.math.BigInteger;
+import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
+import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
+import org.bouncycastle.crypto.params.ECPublicKeyParameters;
+import org.bouncycastle.math.ec.ECPoint;
+import demo.com.util.sm.SM2;
+import demo.com.util.sm.Utils;
+
+/**
+ * 生成SM2密钥对工具
+ */
+public class GenerateKeyPair {
+
+    public static void main(String[] args) {
+        try {
+            System.out.println("========================================");
+            System.out.println("       SM2密钥对生成工具");
+            System.out.println("========================================");
+            System.out.println();
+            
+            // 生成指定数量的密钥对
+            int count = 1;
+            if (args.length > 0) {
+                try {
+                    count = Integer.parseInt(args[0]);
+                } catch (NumberFormatException e) {
+                    count = 1;
+                }
+            }
+            
+            for (int i = 0; i < count; i++) {
+                if (count > 1) {
+                    System.out.println("【密钥对 " + (i + 1) + "】");
+                }
+                
+                SM2 sm2 = SM2.Instance();
+                AsymmetricCipherKeyPair key = sm2.ecc_key_pair_generator.generateKeyPair();
+                ECPrivateKeyParameters ecpriv = (ECPrivateKeyParameters) key.getPrivate();
+                ECPublicKeyParameters ecpub = (ECPublicKeyParameters) key.getPublic();
+                BigInteger privateKey = ecpriv.getD();
+                ECPoint publicKey = ecpub.getQ();
+
+                String priKeyHex = Utils.byteToHex(privateKey.toByteArray());
+                String pubKeyHex = Utils.byteToHex(publicKey.getEncoded());
+                
+                // 确保私钥是64位(32字节)
+                if (priKeyHex.length() > 64) {
+                    priKeyHex = priKeyHex.substring(priKeyHex.length() - 64);
+                }
+                
+                System.out.println("私钥 (64位十六进制):");
+                System.out.println(priKeyHex);
+                System.out.println();
+                
+                System.out.println("公钥 (130位十六进制):");
+                System.out.println(pubKeyHex);
+                System.out.println();
+                
+                // 生成配置文件格式
+                System.out.println("配置文件格式 (config.properties):");
+                System.out.println("----------------------------------------");
+                System.out.println("reqOrgNo=您的机构号");
+                System.out.println("priKey=" + priKeyHex);
+                System.out.println("sltPubKey=" + pubKeyHex);
+                System.out.println("----------------------------------------");
+                System.out.println();
+                
+                // 生成API调用示例
+                System.out.println("API调用示例:");
+                System.out.println("----------------------------------------");
+                System.out.println("curl -X POST http://localhost:8888/api/sign \\");
+                System.out.println("  -H \"Content-Type: application/json\" \\");
+                System.out.println("  -d '{");
+                System.out.println("    \"data\": {\"version\": \"1.0\"},");
+                System.out.println("    \"priKey\": \"" + priKeyHex + "\",");
+                System.out.println("    \"reqOrgNo\": \"YOUR_ORG_ID\"");
+                System.out.println("  }'");
+                System.out.println("----------------------------------------");
+                
+                if (i < count - 1) {
+                    System.out.println();
+                    System.out.println("========================================");
+                    System.out.println();
+                }
+            }
+            
+            System.out.println();
+            System.out.println("========================================");
+            System.out.println("生成完成!请妥善保管私钥!");
+            System.out.println("========================================");
+            
+        } catch (Exception e) {
+            System.err.println("生成密钥对失败: " + e.getMessage());
+            e.printStackTrace();
+        }
+    }
+}
+

+ 394 - 0
src/demo/com/SignServer.java

@@ -0,0 +1,394 @@
+package demo.com;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.net.InetSocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+import java.util.Properties;
+import java.util.TreeMap;
+
+import org.apache.commons.lang.StringUtils;
+import org.bouncycastle.util.encoders.Hex;
+
+import com.alibaba.fastjson.JSONObject;
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpHandler;
+import com.sun.net.httpserver.HttpServer;
+
+import demo.com.util.ByteArrayUtil;
+import demo.com.util.sm.SM2Utils;
+
+/**
+ * HTTP签名服务器
+ * 提供SM2签名接口
+ */
+public class SignServer {
+
+    // 请求机构号
+    private static String reqOrgNo = "201811200001003";
+    // 请求机构的私钥(默认值,建议通过配置文件或环境变量设置)
+    private static String priKey = "3164EE0DF2BCA7A12309383E3305DD6563A28DFE53F65BBD60B3A1D7F80AC275";
+    // 平台给商户的公钥
+    private static String sltPubKey = "046875695CDF1EF046ABB231FDAFA6DCA2AF1E5719EAC00DE80D65FEF03F8485DC9DCBBC10A9A46D565B4CDCEE3510F276209657CAE5BAC10C9678583A44F7F100";
+
+    /**
+     * 从配置文件加载配置
+     */
+    private static void loadConfig(String configFile) {
+        File file = new File(configFile);
+        if (!file.exists()) {
+            return;
+        }
+        
+        try (FileInputStream fis = new FileInputStream(file)) {
+            Properties props = new Properties();
+            props.load(fis);
+            
+            if (props.containsKey("reqOrgNo")) {
+                reqOrgNo = props.getProperty("reqOrgNo");
+            }
+            if (props.containsKey("priKey")) {
+                priKey = props.getProperty("priKey");
+            }
+            if (props.containsKey("sltPubKey")) {
+                sltPubKey = props.getProperty("sltPubKey");
+            }
+            
+            System.out.println("已从配置文件加载配置: " + configFile);
+        } catch (Exception e) {
+            System.err.println("加载配置文件失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 从环境变量加载配置
+     */
+    private static void loadFromEnv() {
+        if (System.getenv("REQ_ORG_NO") != null) {
+            reqOrgNo = System.getenv("REQ_ORG_NO");
+            System.out.println("已从环境变量加载机构号");
+        }
+        if (System.getenv("SM2_PRIVATE_KEY") != null) {
+            priKey = System.getenv("SM2_PRIVATE_KEY");
+            System.out.println("已从环境变量加载私钥");
+        }
+        if (System.getenv("SLT_PUBLIC_KEY") != null) {
+            sltPubKey = System.getenv("SLT_PUBLIC_KEY");
+            System.out.println("已从环境变量加载公钥");
+        }
+    }
+
+    public static void main(String[] args) throws Exception {
+        // 1. 首先尝试加载配置文件
+        String configFile = "config.properties";
+        if (args.length > 0) {
+            configFile = args[0];
+        }
+        loadConfig(configFile);
+        
+        // 2. 从环境变量加载(会覆盖配置文件)
+        loadFromEnv();
+        
+        // 3. 从命令行参数加载(优先级最高)
+        if (args.length >= 3) {
+            reqOrgNo = args[1];
+            priKey = args[2];
+            if (args.length >= 4) {
+                sltPubKey = args[3];
+            }
+            System.out.println("已从命令行参数加载配置");
+        }
+        // 创建HTTP服务器,监听8888端口
+        HttpServer server = HttpServer.create(new InetSocketAddress(8888), 0);
+        
+        // 注册签名接口
+        server.createContext("/api/sign", new SignHandler());
+        // 注册验签接口
+        server.createContext("/api/verify", new VerifyHandler());
+        // 注册健康检查接口
+        server.createContext("/api/health", new HealthHandler());
+        
+        server.setExecutor(null);
+        server.start();
+        
+        System.out.println("=================================================");
+        System.out.println("签名服务器已启动,监听端口: 8888");
+        System.out.println("=================================================");
+        System.out.println("当前配置:");
+        System.out.println("机构号: " + reqOrgNo);
+        System.out.println("私钥: " + maskKey(priKey));
+        System.out.println("公钥: " + maskKey(sltPubKey));
+        System.out.println("=================================================");
+        System.out.println("接口列表:");
+        System.out.println("1. POST http://localhost:8888/api/sign - 签名接口");
+        System.out.println("   请求参数: {\"data\": {key1: value1, key2: value2, ...}}");
+        System.out.println("   返回: {\"success\": true, \"signData\": \"...\", \"sign\": \"...\"}");
+        System.out.println();
+        System.out.println("2. POST http://localhost:8888/api/verify - 验签接口");
+        System.out.println("   请求参数: {\"data\": {key1: value1, ...}, \"sign\": \"...\"}");
+        System.out.println("   返回: {\"success\": true, \"valid\": true/false}");
+        System.out.println();
+        System.out.println("3. GET http://localhost:8888/api/health - 健康检查");
+        System.out.println("=================================================");
+    }
+    
+    /**
+     * 隐藏密钥中间部分(仅用于显示)
+     */
+    private static String maskKey(String key) {
+        if (key == null || key.length() < 16) {
+            return "****";
+        }
+        return key.substring(0, 8) + "..." + key.substring(key.length() - 8);
+    }
+
+    /**
+     * 签名处理器
+     */
+    static class SignHandler implements HttpHandler {
+        @Override
+        public void handle(HttpExchange exchange) throws IOException {
+            // 设置CORS
+            exchange.getResponseHeaders().add("Access-Control-Allow-Origin", "*");
+            exchange.getResponseHeaders().add("Access-Control-Allow-Methods", "POST, OPTIONS");
+            exchange.getResponseHeaders().add("Access-Control-Allow-Headers", "Content-Type");
+            
+            if ("OPTIONS".equals(exchange.getRequestMethod())) {
+                exchange.sendResponseHeaders(200, -1);
+                return;
+            }
+            
+            if (!"POST".equals(exchange.getRequestMethod())) {
+                sendJsonResponse(exchange, 405, createErrorResponse("只支持POST请求"));
+                return;
+            }
+
+            try {
+                // 读取请求体
+                String requestBody = readRequestBody(exchange.getRequestBody());
+                System.out.println("收到签名请求: " + requestBody);
+                
+                // 解析JSON
+                JSONObject jsonRequest = JSONObject.parseObject(requestBody);
+                JSONObject data = jsonRequest.getJSONObject("data");
+                
+                if (data == null || data.isEmpty()) {
+                    sendJsonResponse(exchange, 400, createErrorResponse("data参数不能为空"));
+                    return;
+                }
+                
+                // 获取私钥(优先使用请求中的私钥,否则使用默认私钥)
+                String usePrivateKey = priKey;
+                String useOrgNo = reqOrgNo;
+                
+                if (jsonRequest.containsKey("priKey") && !StringUtils.isBlank(jsonRequest.getString("priKey"))) {
+                    usePrivateKey = jsonRequest.getString("priKey");
+                    System.out.println("使用请求中的私钥: " + maskKey(usePrivateKey));
+                }
+                
+                if (jsonRequest.containsKey("reqOrgNo") && !StringUtils.isBlank(jsonRequest.getString("reqOrgNo"))) {
+                    useOrgNo = jsonRequest.getString("reqOrgNo");
+                    System.out.println("使用请求中的机构号: " + useOrgNo);
+                }
+                
+                // 转换为Map并生成签名数据
+                Map<String, String> dataMap = new TreeMap<>();
+                for (String key : data.keySet()) {
+                    dataMap.put(key, data.getString(key));
+                }
+                
+                // 生成待签名数据
+                String signData = getSignDataByKeyAsc(dataMap);
+                System.out.println("待签名数据: " + signData);
+                
+                // 生成签名
+                byte[] mac = SM2Utils.sign(useOrgNo.getBytes(), Hex.decode(usePrivateKey), signData.getBytes("utf-8"));
+                String sign = ByteArrayUtil.hexEncode(mac);
+                
+                System.out.println("签名结果: " + sign);
+                
+                // 返回结果
+                JSONObject response = new JSONObject();
+                response.put("success", true);
+                response.put("signData", signData);
+                response.put("sign", sign);
+                response.put("timestamp", System.currentTimeMillis());
+                
+                sendJsonResponse(exchange, 200, response.toJSONString());
+                
+            } catch (Exception e) {
+                e.printStackTrace();
+                sendJsonResponse(exchange, 500, createErrorResponse("签名失败: " + e.getMessage()));
+            }
+        }
+    }
+
+    /**
+     * 验签处理器
+     */
+    static class VerifyHandler implements HttpHandler {
+        @Override
+        public void handle(HttpExchange exchange) throws IOException {
+            // 设置CORS
+            exchange.getResponseHeaders().add("Access-Control-Allow-Origin", "*");
+            exchange.getResponseHeaders().add("Access-Control-Allow-Methods", "POST, OPTIONS");
+            exchange.getResponseHeaders().add("Access-Control-Allow-Headers", "Content-Type");
+            
+            if ("OPTIONS".equals(exchange.getRequestMethod())) {
+                exchange.sendResponseHeaders(200, -1);
+                return;
+            }
+            
+            if (!"POST".equals(exchange.getRequestMethod())) {
+                sendJsonResponse(exchange, 405, createErrorResponse("只支持POST请求"));
+                return;
+            }
+
+            try {
+                // 读取请求体
+                String requestBody = readRequestBody(exchange.getRequestBody());
+                System.out.println("收到验签请求: " + requestBody);
+                
+                // 解析JSON
+                JSONObject jsonRequest = JSONObject.parseObject(requestBody);
+                JSONObject data = jsonRequest.getJSONObject("data");
+                String sign = jsonRequest.getString("sign");
+                
+                if (data == null || data.isEmpty() || StringUtils.isBlank(sign)) {
+                    sendJsonResponse(exchange, 400, createErrorResponse("data和sign参数不能为空"));
+                    return;
+                }
+                
+                // 获取公钥(优先使用请求中的公钥,否则使用默认公钥)
+                String usePublicKey = sltPubKey;
+                String useOrgNo = reqOrgNo;
+                
+                if (jsonRequest.containsKey("pubKey") && !StringUtils.isBlank(jsonRequest.getString("pubKey"))) {
+                    usePublicKey = jsonRequest.getString("pubKey");
+                    System.out.println("使用请求中的公钥: " + maskKey(usePublicKey));
+                }
+                
+                if (jsonRequest.containsKey("reqOrgNo") && !StringUtils.isBlank(jsonRequest.getString("reqOrgNo"))) {
+                    useOrgNo = jsonRequest.getString("reqOrgNo");
+                    System.out.println("使用请求中的机构号: " + useOrgNo);
+                }
+                
+                // 转换为Map并生成签名数据
+                Map<String, String> dataMap = new TreeMap<>();
+                for (String key : data.keySet()) {
+                    dataMap.put(key, data.getString(key));
+                }
+                
+                // 生成待验签数据
+                String signData = getSignDataByKeyAsc(dataMap);
+                System.out.println("待验签数据: " + signData);
+                
+                // 验签
+                boolean valid = SM2Utils.verifySign(
+                    useOrgNo.getBytes(), 
+                    Hex.decode(usePublicKey), 
+                    signData.getBytes("utf-8"),
+                    ByteArrayUtil.hexDecode(sign)
+                );
+                
+                System.out.println("验签结果: " + (valid ? "成功" : "失败"));
+                
+                // 返回结果
+                JSONObject response = new JSONObject();
+                response.put("success", true);
+                response.put("valid", valid);
+                response.put("signData", signData);
+                response.put("timestamp", System.currentTimeMillis());
+                
+                sendJsonResponse(exchange, 200, response.toJSONString());
+                
+            } catch (Exception e) {
+                e.printStackTrace();
+                sendJsonResponse(exchange, 500, createErrorResponse("验签失败: " + e.getMessage()));
+            }
+        }
+    }
+
+    /**
+     * 健康检查处理器
+     */
+    static class HealthHandler implements HttpHandler {
+        @Override
+        public void handle(HttpExchange exchange) throws IOException {
+            exchange.getResponseHeaders().add("Access-Control-Allow-Origin", "*");
+            
+            JSONObject response = new JSONObject();
+            response.put("status", "ok");
+            response.put("timestamp", System.currentTimeMillis());
+            
+            sendJsonResponse(exchange, 200, response.toJSONString());
+        }
+    }
+
+    /**
+     * 读取请求体
+     */
+    private static String readRequestBody(InputStream inputStream) throws IOException {
+        BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
+        StringBuilder sb = new StringBuilder();
+        String line;
+        while ((line = reader.readLine()) != null) {
+            sb.append(line);
+        }
+        return sb.toString();
+    }
+
+    /**
+     * 发送JSON响应
+     */
+    private static void sendJsonResponse(HttpExchange exchange, int statusCode, String response) throws IOException {
+        exchange.getResponseHeaders().set("Content-Type", "application/json; charset=UTF-8");
+        byte[] bytes = response.getBytes(StandardCharsets.UTF_8);
+        exchange.sendResponseHeaders(statusCode, bytes.length);
+        OutputStream os = exchange.getResponseBody();
+        os.write(bytes);
+        os.close();
+    }
+
+    /**
+     * 创建错误响应
+     */
+    private static String createErrorResponse(String message) {
+        JSONObject response = new JSONObject();
+        response.put("success", false);
+        response.put("error", message);
+        response.put("timestamp", System.currentTimeMillis());
+        return response.toJSONString();
+    }
+
+    /**
+     * 按key升序生成签名数据
+     */
+    private static String getSignDataByKeyAsc(Map<String, String> map) {
+        String signData = "";
+        if (map == null || map.keySet().isEmpty()) {
+            return "";
+        }
+        Map<String, String> tmp = new TreeMap<>();
+        for (Map.Entry<String, String> en : map.entrySet()) {
+            tmp.put(en.getKey().toUpperCase(), en.getValue());
+        }
+        for (Map.Entry<String, String> en : tmp.entrySet()) {
+            if (!StringUtils.isBlank(en.getValue())) {
+                if (StringUtils.isBlank(signData)) {
+                    signData += en.getKey() + "=" + en.getValue();
+                } else {
+                    signData += "|" + en.getKey() + "=" + en.getValue();
+                }
+            }
+        }
+        return signData;
+    }
+}
+

+ 145 - 0
src/demo/com/Test.java

@@ -0,0 +1,145 @@
+package demo.com;
+
+import java.io.UnsupportedEncodingException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Map;
+import java.util.Random;
+import java.util.TreeMap;
+
+import org.apache.commons.lang.StringUtils;
+import org.bouncycastle.util.encoders.Hex;
+
+import com.alibaba.fastjson.JSONObject;
+
+import demo.com.entity.AgreementTxnRequest;
+import demo.com.entity.AgreementTxnResponse;
+import demo.com.service.IAgreementTxnController;
+import demo.com.util.ByteArrayUtil;
+import demo.com.util.cxf.CxfService;
+import demo.com.util.sm.SM2Utils;
+
+public class Test {
+
+	// 请求机构号
+	private static String reqOrgNo = "201811200001003";
+	// 请求机构密码
+	private static String reqOrgPwd = "200012";
+
+	// 请求机构的私钥
+	private static String priKey = "3164EE0DF2BCA7A12309383E3305DD6563A28DFE53F65BBD60B3A1D7F80AC275";
+
+	// 平台给商户的公钥
+	private static String sltPubKey = "046875695CDF1EF046ABB231FDAFA6DCA2AF1E5719EAC00DE80D65FEF03F8485DC9DCBBC10A9A46D565B4CDCEE3510F276209657CAE5BAC10C9678583A44F7F100";
+
+	private static long ranSalt = 0;
+
+	// 请求流水号
+	private static String getReqSeqNo() {
+
+		SimpleDateFormat dateFormat = new SimpleDateFormat("yyMMddhhmmss");
+		Long timeLong = Long.valueOf(dateFormat.format(new Date()));
+		ranSalt++;
+		Random r = new Random(System.currentTimeMillis() + ranSalt);
+		Long ret = Long.valueOf(r.nextInt(10000));
+		ret = timeLong * 10000 + ret;
+		return ret.toString();
+	}
+
+	public static void main(String[] args) throws Exception {
+		IAgreementTxnController service = new CxfService<IAgreementTxnController>().getService(reqOrgNo, reqOrgPwd,
+				"https://air-card-test.ch.com/BackWebServer/services/securityAgreementTxnController", true,
+				IAgreementTxnController.class);
+
+		// 预存账户实时余额支持接口
+		AgreementTxnRequest txnCntBalRequest = new AgreementTxnRequest();
+		txnCntBalRequest.setVersion("1.0");// 固定值1.0
+		txnCntBalRequest.setSafeModelVersion("3.0");// 固定值1.0
+		txnCntBalRequest.setTxnType("20250");
+		txnCntBalRequest.setSubTxnDist("1");
+		txnCntBalRequest.setReqOrgId(reqOrgNo); // 请求机构号(OTA)
+		txnCntBalRequest.setReqSeqNo(getReqSeqNo()); // 航司请求流水号(唯一)
+		txnCntBalRequest.setAccountCurrencyCode("156");
+		txnCntBalRequest.setMerSettleDate("20211223");
+		txnCntBalRequest.setCurrencyCode("156");
+		txnCntBalRequest.setCardNo("9206068000000010425"); // 预存账户
+		txnCntBalRequest.setAccountCurrencyCode("156");
+
+		// 生成签名
+		String signData = getSignDataByKeyAsc(txnCntBalRequest.getMapForMac());
+		byte[] mac = SM2Utils.sign(reqOrgNo.getBytes(), Hex.decode(priKey), signData.getBytes("utf-8"));
+
+		txnCntBalRequest.setSign(ByteArrayUtil.hexEncode(mac));
+
+		System.out.println("预存账户余额查询请求参数:" + JSONObject.toJSONString(txnCntBalRequest));
+
+		AgreementTxnResponse txnCntBalResponse = null;
+		try {
+			txnCntBalResponse = service.findCountBalance(txnCntBalRequest);
+		} catch (Exception e) {
+			e.printStackTrace();
+			System.out.println("预存账户余额查询处理异常");
+		}
+		System.out.println("预存账户余额查询应答参数:" + JSONObject.toJSONString(txnCntBalResponse));
+
+		if (txnCntBalResponse != null) {
+			// 验签
+			signData = getSignDataByKeyAsc(txnCntBalResponse.getMapForMac());
+			boolean check = SM2Utils.verifySign(reqOrgNo.getBytes(), Hex.decode(sltPubKey), signData.getBytes("utf-8"),
+					ByteArrayUtil.hexDecode(txnCntBalResponse.getSign()));
+			if (check) {
+				System.out.println("验签成功");
+				if (!txnCntBalResponse.getRespCode().equals("00")) {
+					System.out.println("错误码:" + txnCntBalResponse.getRespCode());
+				} else {
+					// 单位分
+					System.out.println("余额:" + txnCntBalResponse.getAccountBalance());
+				}
+			} else {
+				System.out.println("验签失败");
+			}
+		}
+	}
+
+	public static String getSignData(Map<String, String> map) {
+		String signData = "";
+		if (map == null || map.keySet().isEmpty()) {
+			return "";
+		}
+		Map<String, String> tmp = new TreeMap<String, String>();
+		for (Map.Entry<String, String> en : map.entrySet()) {
+			tmp.put(en.getKey().toUpperCase(), en.getValue());
+		}
+		for (Map.Entry<String, String> en : tmp.entrySet()) {
+			if (!StringUtils.isBlank(en.getValue())) {
+				if (StringUtils.isBlank(signData)) {
+					signData += en.getKey() + "=" + en.getValue();
+				} else {
+					signData += "|" + en.getKey() + "=" + en.getValue();
+				}
+			}
+		}
+		return signData;
+	}
+
+	public static String getSignDataByKeyAsc(Map<String, String> map) {
+		String signData = "";
+		if (map == null || map.keySet().isEmpty()) {
+			return "";
+		}
+		Map<String, String> tmp = new TreeMap<String, String>();
+		for (Map.Entry<String, String> en : map.entrySet()) {
+			tmp.put(en.getKey().toUpperCase(), en.getValue());
+		}
+		for (Map.Entry<String, String> en : tmp.entrySet()) {
+			if (!StringUtils.isBlank(en.getValue())) {
+				if (StringUtils.isBlank(signData)) {
+					signData += en.getKey() + "=" + en.getValue();
+				} else {
+					signData += "|" + en.getKey() + "=" + en.getValue();
+				}
+			}
+		}
+		return signData;
+	}
+}

+ 385 - 0
src/demo/com/entity/AgreementTxnRequest.java

@@ -0,0 +1,385 @@
+package demo.com.entity;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class AgreementTxnRequest {
+
+	//版本号
+	private String version;
+	//安全模型版本号
+	private String safeModelVersion;
+	//交易类型
+	private String txnType;
+	//子交易区分
+	private String subTxnDist;
+	//请求机构号
+	private String reqOrgId;
+	//请求流水号
+	private String reqSeqNo;
+	//商家运营日
+	private String merSettleDate;
+	//应答码
+	private String respCode;
+	//签名
+	private String sign;
+	//授权标识
+	private String bindId;
+	//账户类型
+	private String accountCurrencyCode;
+	//返回地址
+	private String backUrl;
+	//商家用户ID
+	private String merUserId;
+	//交易货币代码
+	private String currencyCode;
+	//交易金额
+	private Long txnAmt;
+	//请求商户号
+	private String merchantId;
+	//商品名称/商品编号
+	private String productId;
+	//订单号
+	private String orderId;
+	//原交易流水号
+	private String reqSeqNoOld;
+	//查询类型
+	private String queryType;
+	//交易流水号
+	private String tranReqSeqNo;
+	//查询起始日期
+	private String txnStartTime;
+	//查询截止日期
+	private String txnEndTime;
+	//保留域
+	private String reserved;
+	
+	private String clientId;
+	private String cardNo;
+	private Long accountBalance;
+	
+	private String settleDate;//运营日
+	private String txnStat;//交易状态
+	private String txnTime;//交易时间
+
+	//航司订单号
+	private String retrievalReferenceNumber;
+
+	private String reqOrgOrderId; //授权方业务订单号
+	
+	private String reqOrgSeqNo;
+	
+	private String responseUrl;
+	
+	/**
+	 * 取得参与MAC签名字段信息,以map<字段名,字段值字符串>方式返回
+	 * @return
+	 */
+	public Map<String, String> getMapForMac(){
+		Map<String, String> map = new HashMap<String, String>();
+		map.put("version", getStrValue(this.version));
+		map.put("safeModelVesion", getStrValue(this.safeModelVersion));
+		map.put("txnType", getStrValue(this.txnType));
+		map.put("subTxnDist", getStrValue(this.subTxnDist));
+		map.put("reqOrgId", getStrValue(this.reqOrgId));
+		map.put("reqSeqNo", getStrValue(this.reqSeqNo));
+		map.put("merSettleDate", getStrValue(this.merSettleDate));
+		map.put("bindId", getStrValue(this.bindId));
+		map.put("accountCurrencyCode", getStrValue(this.accountCurrencyCode));
+		map.put("currencyCode", getStrValue(this.currencyCode));
+		map.put("txnAmt", getStrValue(this.txnAmt));
+		map.put("merchantId", getStrValue(this.merchantId));
+		map.put("productId", getStrValue(this.productId));
+		map.put("orderId", getStrValue(this.orderId));
+		map.put("backUrl", getStrValue(this.backUrl));
+		map.put("merUserId", getStrValue(this.merUserId));
+		map.put("reqSeqNoOld", getStrValue(this.reqSeqNoOld));
+		map.put("queryType", getStrValue(this.queryType));
+		map.put("tranReqSeqNo", getStrValue(this.tranReqSeqNo));
+		map.put("txnStartTime", getStrValue(this.txnStartTime));
+		map.put("txnEndTime", getStrValue(this.txnEndTime));
+		map.put("respCode", getStrValue(this.respCode));
+		map.put("cardNo", getStrValue(this.cardNo));
+		return map;
+	}
+	
+	private String getStrValue(Object obj){
+		if(obj == null){
+			return "";
+		} else {
+			return String.valueOf(obj);
+		}
+	}
+	
+	public String getVersion() {
+		return version;
+	}
+
+	public void setVersion(String version) {
+		this.version = version;
+	}
+
+	public String getSafeModelVersion() {
+		return safeModelVersion;
+	}
+
+	public void setSafeModelVersion(String safeModelVersion) {
+		this.safeModelVersion = safeModelVersion;
+	}
+
+	public String getTxnType() {
+		return txnType;
+	}
+
+	public void setTxnType(String txnType) {
+		this.txnType = txnType;
+	}
+
+	public String getSubTxnDist() {
+		return subTxnDist;
+	}
+
+	public void setSubTxnDist(String subTxnDist) {
+		this.subTxnDist = subTxnDist;
+	}
+
+	public String getReqOrgId() {
+		return reqOrgId;
+	}
+
+	public void setReqOrgId(String reqOrgId) {
+		this.reqOrgId = reqOrgId;
+	}
+
+	public String getReqSeqNo() {
+		return reqSeqNo;
+	}
+
+	public void setReqSeqNo(String reqSeqNo) {
+		this.reqSeqNo = reqSeqNo;
+	}
+
+	public String getMerSettleDate() {
+		return merSettleDate;
+	}
+
+	public void setMerSettleDate(String merSettleDate) {
+		this.merSettleDate = merSettleDate;
+	}
+
+	public String getRespCode() {
+		return respCode;
+	}
+
+	public void setRespCode(String respCode) {
+		this.respCode = respCode;
+	}
+
+	public String getSign() {
+		return sign;
+	}
+
+	public void setSign(String sign) {
+		this.sign = sign;
+	}
+
+	public String getBindId() {
+		return bindId;
+	}
+
+	public void setBindId(String bindId) {
+		this.bindId = bindId;
+	}
+
+	public String getAccountCurrencyCode() {
+		return accountCurrencyCode;
+	}
+
+	public void setAccountCurrencyCode(String accountCurrencyCode) {
+		this.accountCurrencyCode = accountCurrencyCode;
+	}
+
+	public String getBackUrl() {
+		return backUrl;
+	}
+
+	public void setBackUrl(String backUrl) {
+		this.backUrl = backUrl;
+	}
+
+	public String getMerUserId() {
+		return merUserId;
+	}
+
+	public void setMerUserId(String merUserId) {
+		this.merUserId = merUserId;
+	}
+
+	public String getReserved() {
+		return reserved;
+	}
+
+	public void setReserved(String reserved) {
+		this.reserved = reserved;
+	}
+
+	public String getCurrencyCode() {
+		return currencyCode;
+	}
+
+	public void setCurrencyCode(String currencyCode) {
+		this.currencyCode = currencyCode;
+	}
+
+	public Long getTxnAmt() {
+		return txnAmt;
+	}
+
+	public void setTxnAmt(Long txnAmt) {
+		this.txnAmt = txnAmt;
+	}
+
+	public String getMerchantId() {
+		return merchantId;
+	}
+
+	public void setMerchantId(String merchantId) {
+		this.merchantId = merchantId;
+	}
+
+	public String getProductId() {
+		return productId;
+	}
+
+	public void setProductId(String productId) {
+		this.productId = productId;
+	}
+
+	public String getOrderId() {
+		return orderId;
+	}
+
+	public void setOrderId(String orderId) {
+		this.orderId = orderId;
+	}
+
+	public String getReqSeqNoOld() {
+		return reqSeqNoOld;
+	}
+
+	public void setReqSeqNoOld(String reqSeqNoOld) {
+		this.reqSeqNoOld = reqSeqNoOld;
+	}
+
+	public String getQueryType() {
+		return queryType;
+	}
+
+	public void setQueryType(String queryType) {
+		this.queryType = queryType;
+	}
+
+	public String getTranReqSeqNo() {
+		return tranReqSeqNo;
+	}
+
+	public void setTranReqSeqNo(String tranReqSeqNo) {
+		this.tranReqSeqNo = tranReqSeqNo;
+	}
+
+	public String getTxnStartTime() {
+		return txnStartTime;
+	}
+
+	public void setTxnStartTime(String txnStartTime) {
+		this.txnStartTime = txnStartTime;
+	}
+
+	public String getTxnEndTime() {
+		return txnEndTime;
+	}
+
+	public void setTxnEndTime(String txnEndTime) {
+		this.txnEndTime = txnEndTime;
+	}
+
+	public String getClientId() {
+		return clientId;
+	}
+
+	public void setClientId(String clientId) {
+		this.clientId = clientId;
+	}
+
+	public String getCardNo() {
+		return cardNo;
+	}
+
+	public void setCardNo(String cardNo) {
+		this.cardNo = cardNo;
+	}
+
+	public String getSettleDate() {
+		return settleDate;
+	}
+
+	public void setSettleDate(String settleDate) {
+		this.settleDate = settleDate;
+	}
+
+	public Long getAccountBalance() {
+		return accountBalance;
+	}
+
+	public void setAccountBalance(Long accountBalance) {
+		this.accountBalance = accountBalance;
+	}
+
+	public String getTxnStat() {
+		return txnStat;
+	}
+
+	public void setTxnStat(String txnStat) {
+		this.txnStat = txnStat;
+	}
+
+	public String getTxnTime() {
+		return txnTime;
+	}
+
+	public void setTxnTime(String txnTime) {
+		this.txnTime = txnTime;
+	}
+
+	public String getRetrievalReferenceNumber() {
+		return retrievalReferenceNumber;
+	}
+
+	public void setRetrievalReferenceNumber(String retrievalReferenceNumber) {
+		this.retrievalReferenceNumber = retrievalReferenceNumber;
+	}
+
+	public String getReqOrgOrderId() {
+		return reqOrgOrderId;
+	}
+
+	public void setReqOrgOrderId(String reqOrgOrderId) {
+		this.reqOrgOrderId = reqOrgOrderId;
+	}
+
+	public String getReqOrgSeqNo() {
+		return reqOrgSeqNo;
+	}
+
+	public void setReqOrgSeqNo(String reqOrgSeqNo) {
+		this.reqOrgSeqNo = reqOrgSeqNo;
+	}
+
+	public String getResponseUrl() {
+		return responseUrl;
+	}
+
+	public void setResponseUrl(String responseUrl) {
+		this.responseUrl = responseUrl;
+	}
+}

+ 278 - 0
src/demo/com/entity/AgreementTxnResponse.java

@@ -0,0 +1,278 @@
+package demo.com.entity;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class AgreementTxnResponse {
+
+	// 版本号
+	private String version;
+	// 安全模型版本号
+	private String safeModelVersion;
+	// 交易类型
+	private String txnType;
+	// 子交易区分
+	private String subTxnDist;
+	// 请求机构号
+	private String reqOrgId;
+	// 请求流水号
+	private String reqSeqNo;
+	// 商家运营日
+	private String merSettleDate;
+	// 应答码
+	private String respCode;
+	// 签名
+	private String sign;
+	// 授权标识
+	private String bindId;
+	// 账户类型
+	private String accountCurrencyCode;
+	// 返回地址
+	private String backUrl;
+	// 商家用户ID
+	private String merUserId;
+	// 用戶ID
+	private String userId;
+	// 用戶名
+	private String userName;
+	// 账户余额
+	private Long accountBalance;
+	// 运营日
+	private String settleDate;
+	// 交易货币代码
+	private String currencyCode;
+	// 交易金额
+	private Long txnAmt;
+	// 请求商户号
+	private String merchantId;
+	// 保留域
+	private String reserved;
+	// 交易状态
+	private String txnStat;
+	// 交易时间
+	private String txnTime;
+
+	/**
+	 * 取得参与MAC签名字段信息,以map<字段名,字段值字符串>方式返回
+	 * 
+	 * @return
+	 */
+	public Map<String, String> getMapForMac() {
+		Map<String, String> map = new HashMap<String, String>();
+		map.put("version", getStrValue(this.version));
+		map.put("safeModelVesion", getStrValue(this.safeModelVersion));
+		map.put("txnType", getStrValue(this.txnType));
+		map.put("subTxnDist", getStrValue(this.subTxnDist));
+		map.put("reqOrgId", getStrValue(this.reqOrgId));
+		map.put("reqSeqNo", getStrValue(this.reqSeqNo));
+		map.put("merSettleDate", getStrValue(this.merSettleDate));
+		map.put("respCode", getStrValue(this.respCode));
+		map.put("bindId", getStrValue(this.bindId));
+		map.put("accountCurrencyCode", getStrValue(this.accountCurrencyCode));
+		map.put("currencyCode", getStrValue(this.currencyCode));
+		map.put("txnAmt", getStrValue(this.txnAmt));
+		map.put("merchantId", getStrValue(this.merchantId));
+		map.put("backUrl", getStrValue(this.backUrl));
+		map.put("merUserId", getStrValue(this.merUserId));
+		map.put("userId", getStrValue(this.userId));
+		map.put("userName", getStrValue(this.userName));
+		map.put("accountBalance", getStrValue(this.accountBalance));
+		map.put("settleDate", getStrValue(this.settleDate));
+		map.put("txnStat", getStrValue(this.txnStat));
+		map.put("txnTime", getStrValue(this.txnTime));
+		return map;
+	}
+
+	private String getStrValue(Object obj) {
+		if (obj == null) {
+			return "";
+		} else {
+			return String.valueOf(obj);
+		}
+	}
+
+	public String getVersion() {
+		return version;
+	}
+
+	public void setVersion(String version) {
+		this.version = version;
+	}
+
+	public String getSafeModelVersion() {
+		return safeModelVersion;
+	}
+
+	public void setSafeModelVersion(String safeModelVersion) {
+		this.safeModelVersion = safeModelVersion;
+	}
+
+	public String getTxnType() {
+		return txnType;
+	}
+
+	public void setTxnType(String txnType) {
+		this.txnType = txnType;
+	}
+
+	public String getSubTxnDist() {
+		return subTxnDist;
+	}
+
+	public void setSubTxnDist(String subTxnDist) {
+		this.subTxnDist = subTxnDist;
+	}
+
+	public String getReqOrgId() {
+		return reqOrgId;
+	}
+
+	public void setReqOrgId(String reqOrgId) {
+		this.reqOrgId = reqOrgId;
+	}
+
+	public String getReqSeqNo() {
+		return reqSeqNo;
+	}
+
+	public void setReqSeqNo(String reqSeqNo) {
+		this.reqSeqNo = reqSeqNo;
+	}
+
+	public String getMerSettleDate() {
+		return merSettleDate;
+	}
+
+	public void setMerSettleDate(String merSettleDate) {
+		this.merSettleDate = merSettleDate;
+	}
+
+	public String getRespCode() {
+		return respCode;
+	}
+
+	public void setRespCode(String respCode) {
+		this.respCode = respCode;
+	}
+
+	public String getSign() {
+		return sign;
+	}
+
+	public void setSign(String sign) {
+		this.sign = sign;
+	}
+
+	public String getBindId() {
+		return bindId;
+	}
+
+	public void setBindId(String bindId) {
+		this.bindId = bindId;
+	}
+
+	public String getAccountCurrencyCode() {
+		return accountCurrencyCode;
+	}
+
+	public void setAccountCurrencyCode(String accountCurrencyCode) {
+		this.accountCurrencyCode = accountCurrencyCode;
+	}
+
+	public String getBackUrl() {
+		return backUrl;
+	}
+
+	public void setBackUrl(String backUrl) {
+		this.backUrl = backUrl;
+	}
+
+	public String getMerUserId() {
+		return merUserId;
+	}
+
+	public void setMerUserId(String merUserId) {
+		this.merUserId = merUserId;
+	}
+
+	public String getUserId() {
+		return userId;
+	}
+
+	public void setUserId(String userId) {
+		this.userId = userId;
+	}
+
+	public String getUserName() {
+		return userName;
+	}
+
+	public void setUserName(String userName) {
+		this.userName = userName;
+	}
+
+	public Long getAccountBalance() {
+		return accountBalance;
+	}
+
+	public void setAccountBalance(Long accountBalance) {
+		this.accountBalance = accountBalance;
+	}
+
+	public String getSettleDate() {
+		return settleDate;
+	}
+
+	public void setSettleDate(String settleDate) {
+		this.settleDate = settleDate;
+	}
+
+	public String getCurrencyCode() {
+		return currencyCode;
+	}
+
+	public void setCurrencyCode(String currencyCode) {
+		this.currencyCode = currencyCode;
+	}
+
+	public Long getTxnAmt() {
+		return txnAmt;
+	}
+
+	public void setTxnAmt(Long txnAmt) {
+		this.txnAmt = txnAmt;
+	}
+
+	public String getMerchantId() {
+		return merchantId;
+	}
+
+	public void setMerchantId(String merchantId) {
+		this.merchantId = merchantId;
+	}
+
+	public String getReserved() {
+		return reserved;
+	}
+
+	public void setReserved(String reserved) {
+		this.reserved = reserved;
+	}
+
+	public String getTxnStat() {
+		return txnStat;
+	}
+
+	public void setTxnStat(String txnStat) {
+		this.txnStat = txnStat;
+	}
+
+	public String getTxnTime() {
+		return txnTime;
+	}
+
+	public void setTxnTime(String txnTime) {
+		this.txnTime = txnTime;
+	}
+
+}

+ 20 - 0
src/demo/com/service/IAgreementTxnController.java

@@ -0,0 +1,20 @@
+package demo.com.service;
+
+import javax.jws.WebParam;
+import javax.jws.WebService;
+
+import demo.com.entity.AgreementTxnRequest;
+import demo.com.entity.AgreementTxnResponse;
+
+@WebService
+public interface IAgreementTxnController {
+
+	/**
+	 * 卡余额查询
+	 * 
+	 * @param txnRequest
+	 * @return
+	 */
+	AgreementTxnResponse findCountBalance(@WebParam(name = "txnRequest") AgreementTxnRequest txnRequest);
+
+}

+ 150 - 0
src/demo/com/util/ByteArrayUtil.java

@@ -0,0 +1,150 @@
+package demo.com.util;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import sun.misc.BASE64Decoder;
+import sun.misc.BASE64Encoder;
+
+public class ByteArrayUtil {
+
+	private final static char[] HEX_CHARS = { '0', '1', '2', '3', '4', '5',
+			'6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };
+	private final static byte[] HEX_CHAR_BYTES = { 0, 1, 2, 3, 4, 5, 6, 7, 8,
+			9, 10, 11, 12, 13, 14, 15 };
+	private static Logger logger = LoggerFactory.getLogger(ByteArrayUtil.class);
+
+	public static String hexEncode(byte[] bytes) {
+		if (null == bytes) {
+			return "";
+		}
+
+		StringBuilder builder = new StringBuilder();
+		for (int i = 0; i < bytes.length; i++) {
+			byte b = bytes[i];
+			builder.append(hexEncode(b));
+		}
+
+		return builder.toString();
+	}
+
+	private static String hexEncode(byte b) {
+		char[] chars = new char[2];
+		chars[0] = HEX_CHARS[(b >>> 4) & 0x0F];
+		chars[1] = HEX_CHARS[b & 0x0F];
+		return new String(chars);
+	}
+
+	public static byte[] hexDecode(String hexString) {
+		if (null == hexString) {
+			return new byte[] {};
+		}
+
+		char[] chars = hexString.toCharArray();
+		byte[] res = new byte[chars.length / 2];
+		int b = 0;
+		for (int c = 0; c < chars.length; c = c + 2) {
+			char[] byteChars = new char[2];
+			byteChars[0] = chars[c];
+			byteChars[1] = chars[c + 1];
+			res[b] = hexDecode(byteChars);
+			b++;
+		}
+
+		return res;
+	}
+
+	public static byte hexDecode(char[] hexChars) {
+		byte b1 = HEX_CHAR_BYTES[(getIdxInHexChars(hexChars[0]))];
+		byte b2 = HEX_CHAR_BYTES[(getIdxInHexChars(hexChars[1]))];
+		byte res = (byte) (((b1 << 4) & 0xFF) | (b2 & 0x0F));
+
+		return res;
+	}
+
+	private static int getIdxInHexChars(char c) {
+		int res = -1;
+		for (int i = 0; i < HEX_CHARS.length; i++) {
+			if (Character.toUpperCase(c) == HEX_CHARS[i]) {
+				res = i;
+				break;
+			}
+		}
+
+		return res;
+	}
+
+	public static String base64Encode(byte[] bytes) {
+		if (null == bytes) {
+			return "";
+		}
+
+		BASE64Encoder base64Encoder = new BASE64Encoder();
+		return base64Encoder.encode(bytes);
+	}
+
+	public static byte[] base64Decode(String base64EncodedString) {
+		if (null == base64EncodedString) {
+			return new byte[] {};
+		}
+		byte[] res = null;
+
+		BASE64Decoder base64Decoder = new BASE64Decoder();
+		try {
+			res = base64Decoder.decodeBuffer(base64EncodedString);
+		} catch (Exception e) {
+			logger.error("BASE64 Decode exception", e);
+		}
+
+		return res;
+	}
+
+	public static byte[] int2Bytes(int num) {
+		byte[] bytes = new byte[4];
+		for (int i = 0; i < 4; i++) {
+			bytes[i] = (byte) (num >>> (24 - i * 8));
+		}
+		return bytes;
+	}
+
+	public static int bytes2Int(byte[] bytes) {
+		int result = 0;
+		int mask = 0xff;
+		for (int i = 0; i < 4; i++) {
+			result = result + ((bytes[i] & mask) << (24 - i * 8));
+		}
+
+		return result;
+	}
+
+	public static byte[] short2Bytes(int num) {
+		byte[] bytes = new byte[2];
+		for (int i = 0; i < 2; i++) {
+			bytes[i] = (byte) (num >>> (8 - i * 8));
+		}
+		return bytes;
+	}
+
+	public static short bytes2Short(byte[] bytes) {
+		int result = 0;
+		short mask = 0xff;
+		for (int i = 0; i < 2; i++) {
+			result = result + ((bytes[i] & mask) << (8 - i * 8));
+		}
+		return (short) result;
+	}
+
+	public static byte[] subByteArray(byte[] btyes, int fromIdx,
+			int subByteArrayLength) {
+		byte[] result = null;
+		if (subByteArrayLength > 0) {
+			result = new byte[subByteArrayLength];
+			int idx = -1;
+			for (int i = fromIdx; i < fromIdx + subByteArrayLength; i++) {
+				result[++idx] = btyes[i];
+			}
+		}
+		return result;
+	}
+
+}

+ 90 - 0
src/demo/com/util/cxf/CxfService.java

@@ -0,0 +1,90 @@
+package demo.com.util.cxf;
+
+import java.io.IOException;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509TrustManager;
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.callback.UnsupportedCallbackException;
+
+import org.apache.cxf.configuration.jsse.TLSClientParameters;
+import org.apache.cxf.endpoint.Client;
+import org.apache.cxf.frontend.ClientProxy;
+import org.apache.cxf.jaxws.JaxWsProxyFactoryBean;
+import org.apache.cxf.transport.http.HTTPConduit;
+import org.apache.cxf.ws.security.wss4j.WSS4JOutInterceptor;
+import org.apache.ws.security.WSPasswordCallback;
+
+public class CxfService<T> {
+
+	@SuppressWarnings("unchecked")
+	public T getService(final String userName, final String passwrod,
+			String url, boolean userCheck, Class<T> serviceClass) {
+
+		JaxWsProxyFactoryBean factory = new JaxWsProxyFactoryBean();
+		if (userCheck) {
+			Map<String, Object> outMap = new HashMap<String, Object>();
+			outMap.put("action", "UsernameToken");
+			outMap.put("user", userName);
+			outMap.put("passwordType", "PasswordText");
+			outMap.put("passwordCallbackRef", new CallbackHandler() {
+				// 设置用户名、密码
+				public void handle(Callback[] callbacks) throws IOException,
+						UnsupportedCallbackException {
+					if (callbacks == null) {
+						return;
+					}
+					for (Callback thisCallback : callbacks) {
+						WSPasswordCallback tmpCallback = (WSPasswordCallback) thisCallback;
+						tmpCallback.setIdentifier(userName);
+						tmpCallback.setPassword(passwrod);
+					}
+				}
+			});
+			WSS4JOutInterceptor wssOut = new WSS4JOutInterceptor(outMap);
+			factory.getOutInterceptors().add(wssOut);
+		}
+
+		// 2. 创建服务stub
+
+		factory.setServiceClass(serviceClass);
+		factory.setAddress(url);
+		// 服务端返回字段在客户端未定义不做校验
+		if (factory.getProperties() == null) {
+			Map<String, Object> properties = new HashMap<String, Object>();
+			factory.setProperties(properties);
+		}
+		factory.getProperties().put("set-jaxb-validation-event-handler",
+				"false");
+
+		T service = (T) factory.create();
+
+		// 3. 设置是否跳过cn验证
+		TLSClientParameters tcp = new TLSClientParameters();
+		tcp.setTrustManagers(new TrustManager[] { new X509TrustManager() {
+			public void checkClientTrusted(X509Certificate[] arg0, String arg1)
+					throws CertificateException {
+			}
+
+			public void checkServerTrusted(X509Certificate[] arg0, String arg1)
+					throws CertificateException {
+			}
+
+			public X509Certificate[] getAcceptedIssuers() {
+				return new X509Certificate[] {};
+			}
+		} });
+		tcp.setDisableCNCheck(true);
+		Client proxy = ClientProxy.getClient(service);
+		HTTPConduit conduit = (HTTPConduit) proxy.getConduit();
+		conduit.setTlsClientParameters(tcp);
+		// 4. 返回
+		return service;
+
+	}
+}

+ 94 - 0
src/demo/com/util/sm/Cipher.java

@@ -0,0 +1,94 @@
+package demo.com.util.sm;
+
+import java.math.BigInteger;
+
+import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
+import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
+import org.bouncycastle.crypto.params.ECPublicKeyParameters;
+import org.bouncycastle.math.ec.ECPoint;
+
+public class Cipher {
+	private int ct;
+	private ECPoint p2;
+	private SM3Digest sm3keybase;
+	private SM3Digest sm3c3;
+	private byte key[];
+	private byte keyOff;
+
+	public Cipher() {
+		this.ct = 1;
+		this.key = new byte[32];
+		this.keyOff = 0;
+	}
+
+	private void Reset() {
+		this.sm3keybase = new SM3Digest();
+		this.sm3c3 = new SM3Digest();
+
+		byte p[] = Utils.byteConvert32Bytes(p2.getX().toBigInteger());
+		this.sm3keybase.update(p, 0, p.length);
+		this.sm3c3.update(p, 0, p.length);
+
+		p = Utils.byteConvert32Bytes(p2.getY().toBigInteger());
+		this.sm3keybase.update(p, 0, p.length);
+		this.ct = 1;
+		NextKey();
+	}
+
+	private void NextKey() {
+		SM3Digest sm3keycur = new SM3Digest(this.sm3keybase);
+		sm3keycur.update((byte) (ct >> 24 & 0xff));
+		sm3keycur.update((byte) (ct >> 16 & 0xff));
+		sm3keycur.update((byte) (ct >> 8 & 0xff));
+		sm3keycur.update((byte) (ct & 0xff));
+		sm3keycur.doFinal(key, 0);
+		this.keyOff = 0;
+		this.ct++;
+	}
+
+	public ECPoint Init_enc(SM2 sm2, ECPoint userKey) {
+		AsymmetricCipherKeyPair key = sm2.ecc_key_pair_generator.generateKeyPair();
+		ECPrivateKeyParameters ecpriv = (ECPrivateKeyParameters) key.getPrivate();
+		ECPublicKeyParameters ecpub = (ECPublicKeyParameters) key.getPublic();
+		BigInteger k = ecpriv.getD();
+		ECPoint c1 = ecpub.getQ();
+		System.out.println("Init_enc K " + k);
+		System.out.println("Init_enc c1 " + c1);
+		this.p2 = userKey.multiply(k);
+		Reset();
+		return c1;
+	}
+
+	public void Encrypt(byte data[]) {
+		this.sm3c3.update(data, 0, data.length);
+		for (int i = 0; i < data.length; i++) {
+			if (keyOff == key.length) {
+				NextKey();
+			}
+			data[i] ^= key[keyOff++];
+		}
+	}
+
+	public void Init_dec(BigInteger userD, ECPoint c1) {
+		this.p2 = c1.multiply(userD);
+		Reset();
+	}
+
+	public void Decrypt(byte data[]) {
+		for (int i = 0; i < data.length; i++) {
+			if (keyOff == key.length) {
+				NextKey();
+			}
+			data[i] ^= key[keyOff++];
+		}
+
+		this.sm3c3.update(data, 0, data.length);
+	}
+
+	public void Dofinal(byte c3[]) {
+		byte p[] = Utils.byteConvert32Bytes(p2.getY().toBigInteger());
+		this.sm3c3.update(p, 0, p.length);
+		this.sm3c3.doFinal(c3, 0);
+		Reset();
+	}
+}

+ 157 - 0
src/demo/com/util/sm/SM2.java

@@ -0,0 +1,157 @@
+package demo.com.util.sm;
+
+import java.io.ByteArrayInputStream;
+import java.math.BigInteger;
+import java.security.SecureRandom;
+import java.util.Arrays;
+import java.util.Enumeration;
+
+import org.bouncycastle.asn1.ASN1EncodableVector;
+import org.bouncycastle.asn1.ASN1InputStream;
+import org.bouncycastle.asn1.ASN1Sequence;
+import org.bouncycastle.asn1.DERInteger;
+import org.bouncycastle.asn1.DERObject;
+import org.bouncycastle.asn1.DERSequence;
+import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
+import org.bouncycastle.crypto.generators.ECKeyPairGenerator;
+import org.bouncycastle.crypto.params.ECDomainParameters;
+import org.bouncycastle.crypto.params.ECKeyGenerationParameters;
+import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
+import org.bouncycastle.crypto.params.ECPublicKeyParameters;
+import org.bouncycastle.math.ec.ECCurve;
+import org.bouncycastle.math.ec.ECFieldElement;
+import org.bouncycastle.math.ec.ECFieldElement.Fp;
+import org.bouncycastle.math.ec.ECPoint;
+import org.bouncycastle.util.encoders.Hex;
+
+public class SM2 {
+
+	public static String[] ecc_param = { "FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFF",
+			"FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFC",
+			"28E9FA9E9D9F5E344D5A9E4BCF6509A7F39789F515AB8F92DDBCBD414D940E93",
+			"FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFF7203DF6B21C6052B53BBF40939D54123",
+			"32C4AE2C1F1981195F9904466A39C9948FE30BBFF2660BE1715A4589334C74C7",
+			"BC3736A2F4F6779C59BDCEE36B692153D0A9877CC62A474002DF32E52139F0A0" };
+
+	public static SM2 Instance() {
+		return new SM2();
+	}
+
+	public final BigInteger ecc_p;
+	public final BigInteger ecc_a;
+	public final BigInteger ecc_b;
+	public final BigInteger ecc_n;
+	public final BigInteger ecc_gx;
+	public final BigInteger ecc_gy;
+	public final ECCurve ecc_curve;
+	public final ECPoint ecc_point_g;
+	public final ECDomainParameters ecc_bc_spec;
+	public final ECKeyPairGenerator ecc_key_pair_generator;
+	public final ECFieldElement ecc_gx_fieldelement;
+	public final ECFieldElement ecc_gy_fieldelement;
+
+	public SM2() {
+		this.ecc_p = new BigInteger(ecc_param[0], 16);
+		this.ecc_a = new BigInteger(ecc_param[1], 16);
+		this.ecc_b = new BigInteger(ecc_param[2], 16);
+		this.ecc_n = new BigInteger(ecc_param[3], 16);
+		this.ecc_gx = new BigInteger(ecc_param[4], 16);
+		this.ecc_gy = new BigInteger(ecc_param[5], 16);
+
+		this.ecc_gx_fieldelement = new Fp(this.ecc_p, this.ecc_gx);
+		this.ecc_gy_fieldelement = new Fp(this.ecc_p, this.ecc_gy);
+
+		this.ecc_curve = new ECCurve.Fp(this.ecc_p, this.ecc_a, this.ecc_b);
+		this.ecc_point_g = new ECPoint.Fp(this.ecc_curve, this.ecc_gx_fieldelement, this.ecc_gy_fieldelement);
+
+		this.ecc_bc_spec = new ECDomainParameters(this.ecc_curve, this.ecc_point_g, this.ecc_n);
+
+		ECKeyGenerationParameters ecc_ecgenparam;
+		ecc_ecgenparam = new ECKeyGenerationParameters(this.ecc_bc_spec, new SecureRandom());
+
+		this.ecc_key_pair_generator = new ECKeyPairGenerator();
+		this.ecc_key_pair_generator.init(ecc_ecgenparam);
+	}
+
+	public byte[] sm2GetZ(byte[] userId, ECPoint userKey) {
+		SM3Digest sm3 = new SM3Digest();
+
+		int len = userId.length * 8;
+		sm3.update((byte) (len >> 8 & 0xFF));
+		sm3.update((byte) (len & 0xFF));
+		sm3.update(userId, 0, userId.length);
+
+		byte[] p = Utils.byteConvert32Bytes(ecc_a);
+		sm3.update(p, 0, p.length);
+
+		p = Utils.byteConvert32Bytes(ecc_b);
+		sm3.update(p, 0, p.length);
+
+		p = Utils.byteConvert32Bytes(ecc_gx);
+		sm3.update(p, 0, p.length);
+
+		p = Utils.byteConvert32Bytes(ecc_gy);
+		sm3.update(p, 0, p.length);
+
+		p = Utils.byteConvert32Bytes(userKey.getX().toBigInteger());
+		sm3.update(p, 0, p.length);
+
+		p = Utils.byteConvert32Bytes(userKey.getY().toBigInteger());
+		sm3.update(p, 0, p.length);
+
+		byte[] md = new byte[sm3.getDigestSize()];
+		sm3.doFinal(md, 0);
+		return md;
+	}
+
+	public byte[] sm2Sign(byte[] md, BigInteger userKey) {
+		BigInteger e = new BigInteger(1, md);
+		BigInteger k = null;
+		ECPoint kp = null;
+		BigInteger r = null;
+		BigInteger s = null;
+		do {
+			do {
+				AsymmetricCipherKeyPair keypair = ecc_key_pair_generator.generateKeyPair();
+				ECPrivateKeyParameters ecpriv = (ECPrivateKeyParameters) keypair.getPrivate();
+				ECPublicKeyParameters ecpub = (ECPublicKeyParameters) keypair.getPublic();
+				k = ecpriv.getD();
+				kp = ecpub.getQ();
+
+				// r
+				r = e.add(kp.getX().toBigInteger());
+				r = r.mod(ecc_n);
+			} while (r.equals(BigInteger.ZERO) || r.add(k).equals(ecc_n));
+
+			// (1 + dA)~-1
+			BigInteger da_1 = userKey.add(BigInteger.ONE);
+			da_1 = da_1.modInverse(ecc_n);
+
+			// s
+			s = r.multiply(userKey);
+			s = k.subtract(s).mod(ecc_n);
+			s = da_1.multiply(s).mod(ecc_n);
+		} while (s.equals(BigInteger.ZERO));
+
+		DERInteger dr = new DERInteger(r);
+		DERInteger ds = new DERInteger(s);
+		ASN1EncodableVector v2 = new ASN1EncodableVector();
+		v2.add(dr);
+		v2.add(ds);
+		return new DERSequence(v2).getDEREncoded();
+	}
+
+	public boolean sm2Verify(byte[] md, ECPoint userKey, BigInteger r, BigInteger s) {
+		BigInteger e = new BigInteger(1, md);
+		BigInteger t = r.add(s).mod(ecc_n);
+		if (t.equals(BigInteger.ZERO)) {
+			return false;
+		} else {
+			ECPoint x1y1 = this.ecc_point_g.multiply(s);
+			x1y1 = x1y1.add(userKey.multiply(t));
+			BigInteger r2 = e.add(x1y1.getX().toBigInteger()).mod(this.ecc_n);
+			return r.equals(r2);
+		}
+	}
+
+}

+ 198 - 0
src/demo/com/util/sm/SM2Utils.java

@@ -0,0 +1,198 @@
+package demo.com.util.sm;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.math.BigInteger;
+import java.util.Enumeration;
+
+import org.bouncycastle.asn1.ASN1InputStream;
+import org.bouncycastle.asn1.ASN1Sequence;
+import org.bouncycastle.asn1.DERInteger;
+import org.bouncycastle.asn1.DERObject;
+import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
+import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
+import org.bouncycastle.crypto.params.ECPublicKeyParameters;
+import org.bouncycastle.math.ec.ECPoint;
+
+public class SM2Utils {
+
+	public static SM2Utils Instance() {
+		return new SM2Utils();
+	}
+
+	// 生成随机秘钥对
+	public static void generateKeyPair() throws Exception {
+		SM2 sm2 = SM2.Instance();
+		AsymmetricCipherKeyPair key = sm2.ecc_key_pair_generator.generateKeyPair();
+		ECPrivateKeyParameters ecpriv = (ECPrivateKeyParameters) key.getPrivate();
+		ECPublicKeyParameters ecpub = (ECPublicKeyParameters) key.getPublic();
+		BigInteger privateKey = ecpriv.getD();
+		ECPoint publicKey = ecpub.getQ();
+
+		System.out.println("公钥:" + Utils.byteToHex(publicKey.getEncoded()));
+		System.out.println("私钥: " + Utils.byteToHex(privateKey.toByteArray()));
+
+	}
+
+	/**
+	 * 公钥的加密
+	 * 
+	 * @param publicKey
+	 * @param data
+	 * @return
+	 * @throws IOException
+	 */
+	public static byte[] encrypt(byte[] publicKey, byte[] data) throws Exception {
+		if (publicKey == null || publicKey.length == 0) {
+			return null;
+		}
+		if (data == null || data.length == 0) {
+			return null;
+		}
+		byte[] source = new byte[data.length];
+		System.arraycopy(data, 0, source, 0, data.length);
+
+		Cipher cipher = new Cipher();
+		SM2 sm2 = SM2.Instance();
+		ECPoint userKey = sm2.ecc_curve.decodePoint(publicKey);
+
+		ECPoint c = cipher.Init_enc(sm2, userKey);
+		cipher.Encrypt(source);
+		byte[] c3 = new byte[32];
+		cipher.Dofinal(c3);
+
+		byte[] c1 = c.getEncoded();
+
+		byte[] result = new byte[c1.length + source.length + c3.length];
+
+		System.arraycopy(c1, 0, result, 0, c1.length);
+		System.arraycopy(source, 0, result, c1.length, source.length);
+		System.arraycopy(c3, 0, result, c1.length + source.length, c3.length);
+
+		return result;
+	}
+
+	/**
+	 * 解密
+	 * 
+	 * @param privateKey
+	 * @param encryptedData
+	 * @return
+	 * @throws IOException
+	 */
+	public static byte[] decrypt(byte[] privateKey, byte[] encryptedData) throws Exception {
+		if (privateKey == null || privateKey.length == 0) {
+			return null;
+		}
+
+		if (encryptedData == null || encryptedData.length == 0) {
+			return null;
+		}
+
+		// 加密字节数组转换为十六进制的字符串 长度变为encryptedData.length * 2
+		String data = Utils.byteToHex(encryptedData);
+		/***
+		 * 分解加密字串 (C1 = C1标志位2位 + C1实体部分128位 = 130) (C3 = C3实体部分64位 = 64) (C2 =
+		 * encryptedData.length * 2 - C1长度 - C2长度)
+		 */
+		byte[] c1Bytes = Utils.hexToByte(data.substring(0, 130));
+		int c2Len = encryptedData.length - 97;
+		byte[] c2 = Utils.hexToByte(data.substring(130, 130 + 2 * c2Len));
+		byte[] c3 = Utils.hexToByte(data.substring(130 + 2 * c2Len, 194 + 2 * c2Len));
+
+		SM2 sm2 = SM2.Instance();
+		BigInteger userD = new BigInteger(1, privateKey);
+
+		// 通过C1实体字节来生成ECPoint
+		ECPoint c1 = sm2.ecc_curve.decodePoint(c1Bytes);
+		Cipher cipher = new Cipher();
+		cipher.Init_dec(userD, c1);
+		cipher.Decrypt(c2);
+		cipher.Dofinal(c3);
+		// 返回解密结果
+		return c2;
+	}
+
+	/**
+	 * 签名
+	 * 
+	 * @param userId
+	 * @param privateKey
+	 * @param sourceData
+	 * @return
+	 * @throws IOException
+	 */
+	public static byte[] sign(byte[] userId, byte[] privateKey, byte[] sourceData) throws Exception {
+		if (privateKey == null || privateKey.length == 0) {
+			return null;
+		}
+
+		if (sourceData == null || sourceData.length == 0) {
+			return null;
+		}
+
+		SM2 sm2 = SM2.Instance();
+		BigInteger userD = new BigInteger(1, privateKey);
+
+		ECPoint userKey = sm2.ecc_point_g.multiply(userD);
+
+		SM3Digest sm3 = new SM3Digest();
+		byte[] z = sm2.sm2GetZ(userId, userKey);
+
+		sm3.update(z, 0, z.length);
+		sm3.update(sourceData, 0, sourceData.length);
+		byte[] md = new byte[32];
+		sm3.doFinal(md, 0);
+
+		return sm2.sm2Sign(md, userD);
+	}
+
+	/**
+	 * 验证签名
+	 * 
+	 * @param userId
+	 * @param publicKey
+	 * @param sourceData
+	 * @param signData
+	 * @return
+	 * @throws IOException
+	 */
+	@SuppressWarnings("unchecked")
+	public static boolean verifySign(byte[] userId, byte[] publicKey, byte[] sourceData, byte[] sign) throws Exception {
+		if (publicKey == null || publicKey.length == 0) {
+			return false;
+		}
+
+		if (sourceData == null || sourceData.length == 0) {
+			return false;
+		}
+
+		SM2 sm2 = SM2.Instance();
+		ECPoint userKey = sm2.ecc_curve.decodePoint(publicKey);
+
+		SM3Digest sm3 = new SM3Digest();
+		byte[] z = sm2.sm2GetZ(userId, userKey);
+		sm3.update(z, 0, z.length);
+		sm3.update(sourceData, 0, sourceData.length);
+		byte[] md = new byte[32];
+		sm3.doFinal(md, 0);
+
+		ByteArrayInputStream bis = new ByteArrayInputStream(sign);
+		ASN1InputStream dis = new ASN1InputStream(bis);
+		DERObject derObj = dis.readObject();
+		Enumeration<DERInteger> e = ((ASN1Sequence) derObj).getObjects();
+		BigInteger r = ((DERInteger) e.nextElement()).getValue();
+		BigInteger s = ((DERInteger) e.nextElement()).getValue();
+
+		return sm2.sm2Verify(md, userKey, r, s);
+	}
+	
+	public static void main(String[] args) {
+		try {
+			SM2Utils.generateKeyPair();
+		} catch (Exception e) {
+			// TODO Auto-generated catch block
+			e.printStackTrace();
+		}
+	}
+}

+ 255 - 0
src/demo/com/util/sm/SM3.java

@@ -0,0 +1,255 @@
+package demo.com.util.sm;
+
+public class SM3 {
+
+	public static final byte[] iv = { 0x73, (byte) 0x80, 0x16, 0x6f, 0x49, 0x14, (byte) 0xb2, (byte) 0xb9, 0x17, 0x24,
+			0x42, (byte) 0xd7, (byte) 0xda, (byte) 0x8a, 0x06, 0x00, (byte) 0xa9, 0x6f, 0x30, (byte) 0xbc, (byte) 0x16,
+			0x31, 0x38, (byte) 0xaa, (byte) 0xe3, (byte) 0x8d, (byte) 0xee, 0x4d, (byte) 0xb0, (byte) 0xfb, 0x0e,
+			0x4e };
+
+	public static int[] Tj = new int[64];
+
+	static {
+		for (int i = 0; i < 16; i++) {
+			Tj[i] = 0x79cc4519;
+		}
+
+		for (int i = 16; i < 64; i++) {
+			Tj[i] = 0x7a879d8a;
+		}
+	}
+
+	public static byte[] CF(byte[] V, byte[] B) {
+		int[] v, b;
+		v = convert(V);
+		b = convert(B);
+		return convert(CF(v, b));
+	}
+
+	private static int[] convert(byte[] arr) {
+		int[] out = new int[arr.length / 4];
+		byte[] tmp = new byte[4];
+		for (int i = 0; i < arr.length; i += 4) {
+			System.arraycopy(arr, i, tmp, 0, 4);
+			out[i / 4] = bigEndianByteToInt(tmp);
+		}
+		return out;
+	}
+
+	private static byte[] convert(int[] arr) {
+		byte[] out = new byte[arr.length * 4];
+		byte[] tmp = null;
+		for (int i = 0; i < arr.length; i++) {
+			tmp = bigEndianIntToByte(arr[i]);
+			System.arraycopy(tmp, 0, out, i * 4, 4);
+		}
+		return out;
+	}
+
+	public static int[] CF(int[] V, int[] B) {
+		int a, b, c, d, e, f, g, h;
+		int ss1, ss2, tt1, tt2;
+		a = V[0];
+		b = V[1];
+		c = V[2];
+		d = V[3];
+		e = V[4];
+		f = V[5];
+		g = V[6];
+		h = V[7];
+
+		int[][] arr = expand(B);
+		int[] w = arr[0];
+		int[] w1 = arr[1];
+
+		for (int j = 0; j < 64; j++) {
+			ss1 = (bitCycleLeft(a, 12) + e + bitCycleLeft(Tj[j], j));
+			ss1 = bitCycleLeft(ss1, 7);
+			ss2 = ss1 ^ bitCycleLeft(a, 12);
+			tt1 = FFj(a, b, c, j) + d + ss2 + w1[j];
+			tt2 = GGj(e, f, g, j) + h + ss1 + w[j];
+			d = c;
+			c = bitCycleLeft(b, 9);
+			b = a;
+			a = tt1;
+			h = g;
+			g = bitCycleLeft(f, 19);
+			f = e;
+			e = P0(tt2);
+
+		}
+
+		int[] out = new int[8];
+		out[0] = a ^ V[0];
+		out[1] = b ^ V[1];
+		out[2] = c ^ V[2];
+		out[3] = d ^ V[3];
+		out[4] = e ^ V[4];
+		out[5] = f ^ V[5];
+		out[6] = g ^ V[6];
+		out[7] = h ^ V[7];
+
+		return out;
+	}
+
+	private static int[][] expand(int[] B) {
+		int W[] = new int[68];
+		int W1[] = new int[64];
+		for (int i = 0; i < B.length; i++) {
+			W[i] = B[i];
+		}
+
+		for (int i = 16; i < 68; i++) {
+			W[i] = P1(W[i - 16] ^ W[i - 9] ^ bitCycleLeft(W[i - 3], 15)) ^ bitCycleLeft(W[i - 13], 7) ^ W[i - 6];
+		}
+
+		for (int i = 0; i < 64; i++) {
+			W1[i] = W[i] ^ W[i + 4];
+		}
+
+		int arr[][] = new int[][] { W, W1 };
+		return arr;
+	}
+
+	private static byte[] bigEndianIntToByte(int num) {
+		return back(Utils.intToBytes(num));
+	}
+
+	private static int bigEndianByteToInt(byte[] bytes) {
+		return Utils.byteToInt(back(bytes));
+	}
+
+	private static int FFj(int X, int Y, int Z, int j) {
+		if (j >= 0 && j <= 15) {
+			return FF1j(X, Y, Z);
+		} else {
+			return FF2j(X, Y, Z);
+		}
+	}
+
+	private static int GGj(int X, int Y, int Z, int j) {
+		if (j >= 0 && j <= 15) {
+			return GG1j(X, Y, Z);
+		} else {
+			return GG2j(X, Y, Z);
+		}
+	}
+
+	// 逻辑位运算函数
+	private static int FF1j(int X, int Y, int Z) {
+		int tmp = X ^ Y ^ Z;
+		return tmp;
+	}
+
+	private static int FF2j(int X, int Y, int Z) {
+		int tmp = ((X & Y) | (X & Z) | (Y & Z));
+		return tmp;
+	}
+
+	private static int GG1j(int X, int Y, int Z) {
+		int tmp = X ^ Y ^ Z;
+		return tmp;
+	}
+
+	private static int GG2j(int X, int Y, int Z) {
+		int tmp = (X & Y) | (~X & Z);
+		return tmp;
+	}
+
+	private static int P0(int X) {
+		int y = rotateLeft(X, 9);
+		y = bitCycleLeft(X, 9);
+		int z = rotateLeft(X, 17);
+		z = bitCycleLeft(X, 17);
+		int t = X ^ y ^ z;
+		return t;
+	}
+
+	private static int P1(int X) {
+		int t = X ^ bitCycleLeft(X, 15) ^ bitCycleLeft(X, 23);
+		return t;
+	}
+
+	/**
+	 * 对最后一个分组字节数据padding
+	 * 
+	 * @param in
+	 * @param bLen 分组个数
+	 * @return
+	 */
+	public static byte[] padding(byte[] in, int bLen) {
+		int k = 448 - (8 * in.length + 1) % 512;
+		if (k < 0) {
+			k = 960 - (8 * in.length + 1) % 512;
+		}
+		k += 1;
+		byte[] padd = new byte[k / 8];
+		padd[0] = (byte) 0x80;
+		long n = in.length * 8 + bLen * 512;
+		byte[] out = new byte[in.length + k / 8 + 64 / 8];
+		int pos = 0;
+		System.arraycopy(in, 0, out, 0, in.length);
+		pos += in.length;
+		System.arraycopy(padd, 0, out, pos, padd.length);
+		pos += padd.length;
+		byte[] tmp = back(Utils.longToBytes(n));
+		System.arraycopy(tmp, 0, out, pos, tmp.length);
+		return out;
+	}
+
+	/**
+	 * 字节数组逆序
+	 * 
+	 * @param in
+	 * @return
+	 */
+	private static byte[] back(byte[] in) {
+		byte[] out = new byte[in.length];
+		for (int i = 0; i < out.length; i++) {
+			out[i] = in[out.length - i - 1];
+		}
+
+		return out;
+	}
+
+	public static int rotateLeft(int x, int n) {
+		return (x << n) | (x >> (32 - n));
+	}
+
+	private static int bitCycleLeft(int n, int bitLen) {
+		bitLen %= 32;
+		byte[] tmp = bigEndianIntToByte(n);
+		int byteLen = bitLen / 8;
+		int len = bitLen % 8;
+		if (byteLen > 0) {
+			tmp = byteCycleLeft(tmp, byteLen);
+		}
+
+		if (len > 0) {
+			tmp = bitSmall8CycleLeft(tmp, len);
+		}
+
+		return bigEndianByteToInt(tmp);
+	}
+
+	private static byte[] bitSmall8CycleLeft(byte[] in, int len) {
+		byte[] tmp = new byte[in.length];
+		int t1, t2, t3;
+		for (int i = 0; i < tmp.length; i++) {
+			t1 = (byte) ((in[i] & 0x000000ff) << len);
+			t2 = (byte) ((in[(i + 1) % tmp.length] & 0x000000ff) >> (8 - len));
+			t3 = (byte) (t1 | t2);
+			tmp[i] = (byte) t3;
+		}
+
+		return tmp;
+	}
+
+	private static byte[] byteCycleLeft(byte[] in, int byteLen) {
+		byte[] tmp = new byte[in.length];
+		System.arraycopy(in, byteLen, tmp, 0, in.length - byteLen);
+		System.arraycopy(in, 0, tmp, in.length - byteLen, byteLen);
+		return tmp;
+	}
+
+}

+ 121 - 0
src/demo/com/util/sm/SM3Digest.java

@@ -0,0 +1,121 @@
+package demo.com.util.sm;
+
+public class SM3Digest {
+	/** SM3值的长度 */
+	private static final int BYTE_LENGTH = 32;
+
+	/** SM3分组长度 */
+	private static final int BLOCK_LENGTH = 64;
+
+	/** 缓冲区长度 */
+	private static final int BUFFER_LENGTH = BLOCK_LENGTH * 1;
+
+	/** 缓冲区 */
+	private byte[] xBuf = new byte[BUFFER_LENGTH];
+
+	/** 缓冲区偏移量 */
+	private int xBufOff;
+
+	/** 初始向量 */
+	private byte[] V = SM3.iv.clone();
+
+	private int cntBlock = 0;
+
+	public SM3Digest() {
+
+	}
+
+	public static SM3Digest Instance() {
+		return new SM3Digest();
+	}
+	
+	public SM3Digest(SM3Digest t) {
+		System.arraycopy(t.xBuf, 0, this.xBuf, 0, t.xBuf.length);
+		this.xBufOff = t.xBufOff;
+		System.arraycopy(t.V, 0, this.V, 0, t.V.length);
+	}
+
+	/**
+	 * SM3结果输出
+	 * 
+	 * @param out    保存SM3结构的缓冲区
+	 * @param outOff 缓冲区偏移量
+	 * @return
+	 */
+	public int doFinal(byte[] out, int outOff) {
+		byte[] tmp = doFinal();
+		System.arraycopy(tmp, 0, out, 0, tmp.length);
+		return BYTE_LENGTH;
+	}
+
+	public void reset() {
+		xBufOff = 0;
+		cntBlock = 0;
+		V = SM3.iv.clone();
+	}
+
+	/**
+	 * 明文输入
+	 * 
+	 * @param in    明文输入缓冲区
+	 * @param inOff 缓冲区偏移量
+	 * @param len   明文长度
+	 */
+	public void update(byte[] in, int inOff, int len) {
+		int partLen = BUFFER_LENGTH - xBufOff;
+		int inputLen = len;
+		int dPos = inOff;
+		if (partLen < inputLen) {
+			System.arraycopy(in, dPos, xBuf, xBufOff, partLen);
+			inputLen -= partLen;
+			dPos += partLen;
+			doUpdate();
+			while (inputLen > BUFFER_LENGTH) {
+				System.arraycopy(in, dPos, xBuf, 0, BUFFER_LENGTH);
+				inputLen -= BUFFER_LENGTH;
+				dPos += BUFFER_LENGTH;
+				doUpdate();
+			}
+		}
+
+		System.arraycopy(in, dPos, xBuf, xBufOff, inputLen);
+		xBufOff += inputLen;
+	}
+
+	private void doUpdate() {
+		byte[] B = new byte[BLOCK_LENGTH];
+		for (int i = 0; i < BUFFER_LENGTH; i += BLOCK_LENGTH) {
+			System.arraycopy(xBuf, i, B, 0, B.length);
+			doHash(B);
+		}
+		xBufOff = 0;
+	}
+
+	private void doHash(byte[] B) {
+		byte[] tmp = SM3.CF(V, B);
+		System.arraycopy(tmp, 0, V, 0, V.length);
+		cntBlock++;
+	}
+
+	private byte[] doFinal() {
+		byte[] B = new byte[BLOCK_LENGTH];
+		byte[] buffer = new byte[xBufOff];
+		System.arraycopy(xBuf, 0, buffer, 0, buffer.length);
+		byte[] tmp = SM3.padding(buffer, cntBlock);
+		for (int i = 0; i < tmp.length; i += BLOCK_LENGTH) {
+			System.arraycopy(tmp, i, B, 0, B.length);
+			doHash(B);
+		}
+		return V;
+	}
+
+	public void update(byte in) {
+		byte[] buffer = new byte[] { in };
+		update(buffer, 0, 1);
+	}
+
+	public int getDigestSize() {
+		return BYTE_LENGTH;
+	}
+
+}

+ 45 - 0
src/demo/com/util/sm/SM3Utils.java

@@ -0,0 +1,45 @@
+package demo.com.util.sm;
+
+import java.nio.charset.StandardCharsets;
+
+import org.bouncycastle.util.encoders.Hex;
+
+public class SM3Utils {
+
+	public static byte[] sm3(String text, String charset) throws Exception {
+
+		if (charset == null || charset.trim() == "") {
+
+			charset = StandardCharsets.UTF_8.name();
+		}
+
+		byte[] result = new byte[32];
+
+		byte[] data = text.getBytes(charset);
+
+		SM3Digest sm3 = SM3Digest.Instance();
+		sm3.update(data, 0, data.length);
+		sm3.doFinal(result, 0);
+
+		return result;
+	}
+
+	public static String hexSm3(String text, String charset) throws Exception {
+
+		if (charset == null || charset.trim() == "") {
+
+			charset = StandardCharsets.UTF_8.name();
+		}
+
+		byte[] result = new byte[32];
+
+		byte[] data = text.getBytes(charset);
+
+		SM3Digest sm3 = SM3Digest.Instance();
+		sm3.update(data, 0, data.length);
+		sm3.doFinal(result, 0);
+
+		return new String(Hex.encode(result));
+	}
+
+}

+ 612 - 0
src/demo/com/util/sm/Utils.java

@@ -0,0 +1,612 @@
+package demo.com.util.sm;
+
+import java.math.BigInteger;
+
+public class Utils {
+	/**
+	 * 整形转换成网络传输的字节流(字节数组)型数据
+	 * 
+	 * @param num 一个整型数据
+	 * @return 4个字节的自己数组
+	 */
+	public static byte[] intToBytes(int num) {
+		byte[] bytes = new byte[4];
+		bytes[0] = (byte) (0xff & (num >> 0));
+		bytes[1] = (byte) (0xff & (num >> 8));
+		bytes[2] = (byte) (0xff & (num >> 16));
+		bytes[3] = (byte) (0xff & (num >> 24));
+		return bytes;
+	}
+
+	/**
+	 * 四个字节的字节数据转换成一个整形数据
+	 * 
+	 * @param bytes 4个字节的字节数组
+	 * @return 一个整型数据
+	 */
+	public static int byteToInt(byte[] bytes) {
+		int num = 0;
+		int temp;
+		temp = (0x000000ff & (bytes[0])) << 0;
+		num = num | temp;
+		temp = (0x000000ff & (bytes[1])) << 8;
+		num = num | temp;
+		temp = (0x000000ff & (bytes[2])) << 16;
+		num = num | temp;
+		temp = (0x000000ff & (bytes[3])) << 24;
+		num = num | temp;
+		return num;
+	}
+
+	/**
+	 * 长整形转换成网络传输的字节流(字节数组)型数据
+	 * 
+	 * @param num 一个长整型数据
+	 * @return 4个字节的自己数组
+	 */
+	public static byte[] longToBytes(long num) {
+		byte[] bytes = new byte[8];
+		for (int i = 0; i < 8; i++) {
+			bytes[i] = (byte) (0xff & (num >> (i * 8)));
+		}
+
+		return bytes;
+	}
+
+	/**
+	 * 大数字转换字节流(字节数组)型数据
+	 * 
+	 * @param n
+	 * @return
+	 */
+	public static byte[] byteConvert32Bytes(BigInteger n) {
+		byte tmpd[] = (byte[]) null;
+		if (n == null) {
+			return null;
+		}
+
+		if (n.toByteArray().length == 33) {
+			tmpd = new byte[32];
+			System.arraycopy(n.toByteArray(), 1, tmpd, 0, 32);
+		} else if (n.toByteArray().length == 32) {
+			tmpd = n.toByteArray();
+		} else {
+			tmpd = new byte[32];
+			for (int i = 0; i < 32 - n.toByteArray().length; i++) {
+				tmpd[i] = 0;
+			}
+			System.arraycopy(n.toByteArray(), 0, tmpd, 32 - n.toByteArray().length, n.toByteArray().length);
+		}
+		return tmpd;
+	}
+
+	/**
+	 * 换字节流(字节数组)型数据转大数字
+	 * 
+	 * @param b
+	 * @return
+	 */
+	public static BigInteger byteConvertInteger(byte[] b) {
+		if (b[0] < 0) {
+			byte[] temp = new byte[b.length + 1];
+			temp[0] = 0;
+			System.arraycopy(b, 0, temp, 1, b.length);
+			return new BigInteger(temp);
+		}
+		return new BigInteger(b);
+	}
+
+	/**
+	 * 根据字节数组获得值(十六进制数字)
+	 * 
+	 * @param bytes
+	 * @return
+	 */
+	public static String getHexString(byte[] bytes) {
+		return getHexString(bytes, true);
+	}
+
+	/**
+	 * 根据字节数组获得值(十六进制数字)
+	 * 
+	 * @param bytes
+	 * @param upperCase
+	 * @return
+	 */
+	public static String getHexString(byte[] bytes, boolean upperCase) {
+		String ret = "";
+		for (int i = 0; i < bytes.length; i++) {
+			ret += Integer.toString((bytes[i] & 0xff) + 0x100, 16).substring(1);
+		}
+		return upperCase ? ret.toUpperCase() : ret;
+	}
+
+	/**
+	 * 打印十六进制字符串
+	 * 
+	 * @param bytes
+	 */
+	public static void printHexString(byte[] bytes) {
+		for (int i = 0; i < bytes.length; i++) {
+			String hex = Integer.toHexString(bytes[i] & 0xFF);
+			if (hex.length() == 1) {
+				hex = '0' + hex;
+			}
+			System.out.print("0x" + hex.toUpperCase() + ",");
+		}
+		System.out.println("");
+	}
+
+	/**
+	 * Convert hex string to byte[]
+	 * 
+	 * @param hexString the hex string
+	 * @return byte[]
+	 */
+	public static byte[] hexStringToBytes(String hexString) {
+		if (hexString == null || hexString.equals("")) {
+			return null;
+		}
+
+		hexString = hexString.toUpperCase();
+		int length = hexString.length() / 2;
+		char[] hexChars = hexString.toCharArray();
+		byte[] d = new byte[length];
+		for (int i = 0; i < length; i++) {
+			int pos = i * 2;
+			d[i] = (byte) (charToByte(hexChars[pos]) << 4 | charToByte(hexChars[pos + 1]));
+		}
+		return d;
+	}
+
+	/**
+	 * Convert char to byte
+	 * 
+	 * @param c char
+	 * @return byte
+	 */
+	public static byte charToByte(char c) {
+		return (byte) "0123456789ABCDEF".indexOf(c);
+	}
+
+	/**
+	 * 用于建立十六进制字符的输出的小写字符数组
+	 */
+	private static final char[] DIGITS_LOWER = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd',
+			'e', 'f' };
+
+	/**
+	 * 用于建立十六进制字符的输出的大写字符数组
+	 */
+	private static final char[] DIGITS_UPPER = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D',
+			'E', 'F' };
+
+	/**
+	 * 将字节数组转换为十六进制字符数组
+	 *
+	 * @param data byte[]
+	 * @return 十六进制char[]
+	 */
+	public static char[] encodeHex(byte[] data) {
+		return encodeHex(data, true);
+	}
+
+	/**
+	 * 将字节数组转换为十六进制字符数组
+	 *
+	 * @param data        byte[]
+	 * @param toLowerCase <code>true</code> 传换成小写格式 , <code>false</code> 传换成大写格式
+	 * @return 十六进制char[]
+	 */
+	public static char[] encodeHex(byte[] data, boolean toLowerCase) {
+		return encodeHex(data, toLowerCase ? DIGITS_LOWER : DIGITS_UPPER);
+	}
+
+	/**
+	 * 将字节数组转换为十六进制字符数组
+	 *
+	 * @param data     byte[]
+	 * @param toDigits 用于控制输出的char[]
+	 * @return 十六进制char[]
+	 */
+	protected static char[] encodeHex(byte[] data, char[] toDigits) {
+		int l = data.length;
+		char[] out = new char[l << 1];
+		// two characters form the hex value.
+		for (int i = 0, j = 0; i < l; i++) {
+			out[j++] = toDigits[(0xF0 & data[i]) >>> 4];
+			out[j++] = toDigits[0x0F & data[i]];
+		}
+		return out;
+	}
+
+	/**
+	 * 将字节数组转换为十六进制字符串
+	 *
+	 * @param data byte[]
+	 * @return 十六进制String
+	 */
+	public static String encodeHexString(byte[] data) {
+		return encodeHexString(data, true);
+	}
+
+	/**
+	 * 将字节数组转换为十六进制字符串
+	 *
+	 * @param data        byte[]
+	 * @param toLowerCase <code>true</code> 传换成小写格式 , <code>false</code> 传换成大写格式
+	 * @return 十六进制String
+	 */
+	public static String encodeHexString(byte[] data, boolean toLowerCase) {
+		return encodeHexString(data, toLowerCase ? DIGITS_LOWER : DIGITS_UPPER);
+	}
+
+	/**
+	 * 将字节数组转换为十六进制字符串
+	 *
+	 * @param data     byte[]
+	 * @param toDigits 用于控制输出的char[]
+	 * @return 十六进制String
+	 */
+	protected static String encodeHexString(byte[] data, char[] toDigits) {
+		return new String(encodeHex(data, toDigits));
+	}
+
+	/**
+	 * 将十六进制字符数组转换为字节数组
+	 *
+	 * @param data 十六进制char[]
+	 * @return byte[]
+	 * @throws RuntimeException 如果源十六进制字符数组是一个奇怪的长度,将抛出运行时异常
+	 */
+	public static byte[] decodeHex(char[] data) {
+		int len = data.length;
+
+		if ((len & 0x01) != 0) {
+			throw new RuntimeException("Odd number of characters.");
+		}
+
+		byte[] out = new byte[len >> 1];
+
+		// two characters form the hex value.
+		for (int i = 0, j = 0; j < len; i++) {
+			int f = toDigit(data[j], j) << 4;
+			j++;
+			f = f | toDigit(data[j], j);
+			j++;
+			out[i] = (byte) (f & 0xFF);
+		}
+
+		return out;
+	}
+
+	/**
+	 * 将十六进制字符转换成一个整数
+	 *
+	 * @param ch    十六进制char
+	 * @param index 十六进制字符在字符数组中的位置
+	 * @return 一个整数
+	 * @throws RuntimeException 当ch不是一个合法的十六进制字符时,抛出运行时异常
+	 */
+	protected static int toDigit(char ch, int index) {
+		int digit = Character.digit(ch, 16);
+		if (digit == -1) {
+			throw new RuntimeException("Illegal hexadecimal character " + ch + " at index " + index);
+		}
+		return digit;
+	}
+
+	/**
+	 * 数字字符串转ASCII码字符串
+	 * 
+	 * @param String 字符串
+	 * @return ASCII字符串
+	 */
+	public static String StringToAsciiString(String content) {
+		String result = "";
+		int max = content.length();
+		for (int i = 0; i < max; i++) {
+			char c = content.charAt(i);
+			String b = Integer.toHexString(c);
+			result = result + b;
+		}
+		return result;
+	}
+
+	/**
+	 * 十六进制转字符串
+	 * 
+	 * @param hexString  十六进制字符串
+	 * @param encodeType 编码类型4:Unicode,2:普通编码
+	 * @return 字符串
+	 */
+	public static String hexStringToString(String hexString, int encodeType) {
+		String result = "";
+		int max = hexString.length() / encodeType;
+		for (int i = 0; i < max; i++) {
+			char c = (char) hexStringToAlgorism(hexString.substring(i * encodeType, (i + 1) * encodeType));
+			result += c;
+		}
+		return result;
+	}
+
+	/**
+	 * 十六进制字符串装十进制
+	 * 
+	 * @param hex 十六进制字符串
+	 * @return 十进制数值
+	 */
+	public static int hexStringToAlgorism(String hex) {
+		hex = hex.toUpperCase();
+		int max = hex.length();
+		int result = 0;
+		for (int i = max; i > 0; i--) {
+			char c = hex.charAt(i - 1);
+			int algorism = 0;
+			if (c >= '0' && c <= '9') {
+				algorism = c - '0';
+			} else {
+				algorism = c - 55;
+			}
+			result += Math.pow(16, max - i) * algorism;
+		}
+		return result;
+	}
+
+	/**
+	 * 十六转二进制
+	 * 
+	 * @param hex 十六进制字符串
+	 * @return 二进制字符串
+	 */
+	public static String hexStringToBinary(String hex) {
+		hex = hex.toUpperCase();
+		String result = "";
+		int max = hex.length();
+		for (int i = 0; i < max; i++) {
+			char c = hex.charAt(i);
+			switch (c) {
+			case '0':
+				result += "0000";
+				break;
+			case '1':
+				result += "0001";
+				break;
+			case '2':
+				result += "0010";
+				break;
+			case '3':
+				result += "0011";
+				break;
+			case '4':
+				result += "0100";
+				break;
+			case '5':
+				result += "0101";
+				break;
+			case '6':
+				result += "0110";
+				break;
+			case '7':
+				result += "0111";
+				break;
+			case '8':
+				result += "1000";
+				break;
+			case '9':
+				result += "1001";
+				break;
+			case 'A':
+				result += "1010";
+				break;
+			case 'B':
+				result += "1011";
+				break;
+			case 'C':
+				result += "1100";
+				break;
+			case 'D':
+				result += "1101";
+				break;
+			case 'E':
+				result += "1110";
+				break;
+			case 'F':
+				result += "1111";
+				break;
+			}
+		}
+		return result;
+	}
+
+	/**
+	 * ASCII码字符串转数字字符串
+	 * 
+	 * @param String ASCII字符串
+	 * @return 字符串
+	 */
+	public static String AsciiStringToString(String content) {
+		String result = "";
+		int length = content.length() / 2;
+		for (int i = 0; i < length; i++) {
+			String c = content.substring(i * 2, i * 2 + 2);
+			int a = hexStringToAlgorism(c);
+			char b = (char) a;
+			String d = String.valueOf(b);
+			result += d;
+		}
+		return result;
+	}
+
+	/**
+	 * 将十进制转换为指定长度的十六进制字符串
+	 * 
+	 * @param algorism  int 十进制数字
+	 * @param maxLength int 转换后的十六进制字符串长度
+	 * @return String 转换后的十六进制字符串
+	 */
+	public static String algorismToHexString(int algorism, int maxLength) {
+		String result = "";
+		result = Integer.toHexString(algorism);
+
+		if (result.length() % 2 == 1) {
+			result = "0" + result;
+		}
+		return patchHexString(result.toUpperCase(), maxLength);
+	}
+
+	/**
+	 * 字节数组转为普通字符串(ASCII对应的字符)
+	 * 
+	 * @param bytearray byte[]
+	 * @return String
+	 */
+	public static String byteToString(byte[] bytearray) {
+		String result = "";
+		char temp;
+
+		int length = bytearray.length;
+		for (int i = 0; i < length; i++) {
+			temp = (char) bytearray[i];
+			result += temp;
+		}
+		return result;
+	}
+
+	/**
+	 * 二进制字符串转十进制
+	 * 
+	 * @param binary 二进制字符串
+	 * @return 十进制数值
+	 */
+	public static int binaryToAlgorism(String binary) {
+		int max = binary.length();
+		int result = 0;
+		for (int i = max; i > 0; i--) {
+			char c = binary.charAt(i - 1);
+			int algorism = c - '0';
+			result += Math.pow(2, max - i) * algorism;
+		}
+		return result;
+	}
+
+	/**
+	 * 十进制转换为十六进制字符串
+	 * 
+	 * @param algorism int 十进制的数字
+	 * @return String 对应的十六进制字符串
+	 */
+	public static String algorismToHEXString(int algorism) {
+		String result = "";
+		result = Integer.toHexString(algorism);
+
+		if (result.length() % 2 == 1) {
+			result = "0" + result;
+
+		}
+		result = result.toUpperCase();
+
+		return result;
+	}
+
+	/**
+	 * HEX字符串前补0,主要用于长度位数不足。
+	 * 
+	 * @param str       String 需要补充长度的十六进制字符串
+	 * @param maxLength int 补充后十六进制字符串的长度
+	 * @return 补充结果
+	 */
+	static public String patchHexString(String str, int maxLength) {
+		String temp = "";
+		for (int i = 0; i < maxLength - str.length(); i++) {
+			temp = "0" + temp;
+		}
+		str = (temp + str).substring(0, maxLength);
+		return str;
+	}
+
+	/**
+	 * 将一个字符串转换为int
+	 * 
+	 * @param s          String 要转换的字符串
+	 * @param defaultInt int 如果出现异常,默认返回的数字
+	 * @param radix      int 要转换的字符串是什么进制的,如16 8 10.
+	 * @return int 转换后的数字
+	 */
+	public static int parseToInt(String s, int defaultInt, int radix) {
+		int i = 0;
+		try {
+			i = Integer.parseInt(s, radix);
+		} catch (NumberFormatException ex) {
+			i = defaultInt;
+		}
+		return i;
+	}
+
+	/**
+	 * 将一个十进制形式的数字字符串转换为int
+	 * 
+	 * @param s          String 要转换的字符串
+	 * @param defaultInt int 如果出现异常,默认返回的数字
+	 * @return int 转换后的数字
+	 */
+	public static int parseToInt(String s, int defaultInt) {
+		int i = 0;
+		try {
+			i = Integer.parseInt(s);
+		} catch (NumberFormatException ex) {
+			i = defaultInt;
+		}
+		return i;
+	}
+
+	/**
+	 * 十六进制串转化为byte数组
+	 * 
+	 * @return the array of byte
+	 */
+	public static byte[] hexToByte(String hex) throws IllegalArgumentException {
+		if (hex.length() % 2 != 0) {
+			throw new IllegalArgumentException();
+		}
+		char[] arr = hex.toCharArray();
+		byte[] b = new byte[hex.length() / 2];
+		for (int i = 0, j = 0, l = hex.length(); i < l; i++, j++) {
+			String swap = "" + arr[i++] + arr[i];
+			int byteint = Integer.parseInt(swap, 16) & 0xFF;
+			b[j] = new Integer(byteint).byteValue();
+		}
+		return b;
+	}
+
+	/**
+	 * 字节数组转换为十六进制字符串
+	 * 
+	 * @param b byte[] 需要转换的字节数组
+	 * @return String 十六进制字符串
+	 */
+	public static String byteToHex(byte b[]) {
+		if (b == null) {
+			throw new IllegalArgumentException("Argument b ( byte array ) is null! ");
+		}
+		String hs = "";
+		String stmp = "";
+		for (int n = 0; n < b.length; n++) {
+			stmp = Integer.toHexString(b[n] & 0xff);
+			if (stmp.length() == 1) {
+				hs = hs + "0" + stmp;
+			} else {
+				hs = hs + stmp;
+			}
+		}
+		return hs.toUpperCase();
+	}
+
+	public static byte[] subByte(byte[] input, int startIndex, int length) {
+		byte[] bt = new byte[length];
+		for (int i = 0; i < length; i++) {
+			bt[i] = input[i + startIndex];
+		}
+		return bt;
+	}
+}

+ 80 - 0
快速部署.txt

@@ -0,0 +1,80 @@
+╔══════════════════════════════════════════════════════════════╗
+║          SM2签名服务 - Docker快速部署指南                   ║
+╚══════════════════════════════════════════════════════════════╝
+
+【最简单的部署方式 - 3步搞定】
+
+1️⃣  上传项目到服务器
+   scp -r xingfutong-java/ user@server:/opt/
+
+2️⃣  进入项目目录
+   cd /opt/xingfutong-java
+
+3️⃣  一键部署
+   ./deploy.sh
+
+   或使用docker-compose:
+   docker-compose up -d
+
+完成! 服务运行在 http://服务器IP:8888
+
+────────────────────────────────────────────────────────────
+
+【前置要求】
+✅ 服务器只需要Docker,不需要Java!
+
+安装Docker (如果未安装):
+  curl -fsSL https://get.docker.com | sh
+  systemctl start docker
+
+────────────────────────────────────────────────────────────
+
+【测试服务】
+curl http://localhost:8888/api/health
+
+curl -X POST http://localhost:8888/api/sign \
+  -H "Content-Type: application/json" \
+  -d '{"data": {"version": "1.0"}}'
+
+────────────────────────────────────────────────────────────
+
+【常用命令】
+查看日志:  docker logs -f sm2-sign-server
+停止服务:  docker stop sm2-sign-server
+启动服务:  docker start sm2-sign-server
+重启服务:  docker restart sm2-sign-server
+查看状态:  docker ps
+
+────────────────────────────────────────────────────────────
+
+【传入自定义密钥】
+docker run -d \
+  --name sm2-sign-server \
+  -p 8888:8888 \
+  -e SM2_PRIVATE_KEY="您的私钥" \
+  -e REQ_ORG_NO="您的机构号" \
+  sign-server:latest
+
+────────────────────────────────────────────────────────────
+
+【修改端口】
+使用其他端口 (比如80):
+  docker run -d -p 80:8888 ... 
+  访问: http://服务器IP/api/sign
+
+────────────────────────────────────────────────────────────
+
+【更新部署】
+docker-compose up -d --build
+
+────────────────────────────────────────────────────────────
+
+【镜像信息】
+基础镜像: openjdk:8-jre-alpine
+镜像大小: ~85MB
+启动时间: ~3秒
+
+────────────────────────────────────────────────────────────
+
+详细文档请查看: 部署说明.md
+

+ 496 - 0
故障排除.md

@@ -0,0 +1,496 @@
+# Docker部署故障排除指南
+
+## 常见问题与解决方案
+
+### 1. 端口被占用 ⚠️
+
+**错误信息**:
+```
+Ports are not available: exposing port TCP 0.0.0.0:8888 -> 0.0.0.0:0: 
+listen tcp 0.0.0.0:8888: bind: address already in use
+```
+
+**原因**: 8888端口已被其他进程占用
+
+**解决方案A - 停止占用进程**:
+```bash
+# 1. 查找占用端口的进程
+lsof -i :8888
+# 或
+netstat -tlnp | grep 8888
+
+# 2. 停止进程(替换PID为实际进程ID)
+kill <PID>
+
+# 3. 重新启动Docker
+docker-compose up -d
+```
+
+**解决方案B - 更换端口**:
+```bash
+# 修改 docker-compose.yml
+ports:
+  - "9999:8888"  # 使用9999端口
+
+# 或使用命令行
+docker run -d -p 9999:8888 sign-server:latest
+
+# 访问地址变为: http://localhost:9999
+```
+
+---
+
+### 2. 容器名称冲突 ⚠️
+
+**错误信息**:
+```
+The container name "/sm2-sign-server" is already in use
+```
+
+**解决方案**:
+```bash
+# 删除旧容器
+docker rm -f sm2-sign-server
+
+# 重新启动
+docker-compose up -d
+```
+
+---
+
+### 3. 镜像构建失败 ⚠️
+
+**错误信息**:
+```
+ERROR: failed to solve: failed to compute cache key
+```
+
+**可能原因**: bin目录或lib目录不存在
+
+**解决方案**:
+```bash
+# 1. 确保已编译(在本地有Java环境时)
+javac -encoding UTF-8 -d bin -cp "lib/*" $(find src -name "*.java")
+
+# 2. 检查目录结构
+ls -la bin/
+ls -la lib/
+
+# 3. 重新构建
+docker-compose build --no-cache
+docker-compose up -d
+```
+
+---
+
+### 4. 容器启动后立即退出 ⚠️
+
+**检查方法**:
+```bash
+# 查看容器状态
+docker ps -a
+
+# 查看日志
+docker logs sm2-sign-server
+```
+
+**常见原因和解决方案**:
+
+**原因1: Java版本不兼容**
+```bash
+# 修改Dockerfile使用不同的Java版本
+FROM openjdk:11-jre-slim  # 改用Java 11
+```
+
+**原因2: 类文件缺失**
+```bash
+# 确保所有class文件存在
+find bin -name "*.class"
+
+# 如果缺失,重新编译
+javac -encoding UTF-8 -d bin -cp "lib/*" $(find src -name "*.java")
+```
+
+---
+
+### 5. 无法访问服务 ⚠️
+
+**症状**: curl无法连接或超时
+
+**排查步骤**:
+
+**步骤1: 检查容器是否运行**
+```bash
+docker ps
+# 确保STATUS显示"Up"
+```
+
+**步骤2: 检查端口映射**
+```bash
+docker port sm2-sign-server
+# 应该显示: 8888/tcp -> 0.0.0.0:8888
+```
+
+**步骤3: 检查容器日志**
+```bash
+docker logs sm2-sign-server
+# 查看是否有错误信息
+```
+
+**步骤4: 测试容器内部**
+```bash
+# 进入容器
+docker exec -it sm2-sign-server sh
+
+# 在容器内测试
+wget -O- http://localhost:8888/api/health
+```
+
+**步骤5: 检查防火墙**
+```bash
+# CentOS/RHEL
+firewall-cmd --list-ports
+
+# Ubuntu
+ufw status
+
+# 如需开放端口
+firewall-cmd --permanent --add-port=8888/tcp
+firewall-cmd --reload
+```
+
+---
+
+### 6. Docker未安装 ⚠️
+
+**错误信息**:
+```
+docker: command not found
+```
+
+**解决方案**:
+```bash
+# 安装Docker (Linux)
+curl -fsSL https://get.docker.com | sh
+systemctl start docker
+systemctl enable docker
+
+# 验证安装
+docker --version
+docker-compose --version
+```
+
+---
+
+### 7. 权限不足 ⚠️
+
+**错误信息**:
+```
+permission denied while trying to connect to the Docker daemon
+```
+
+**解决方案**:
+```bash
+# 方案A: 添加用户到docker组
+sudo usermod -aG docker $USER
+# 重新登录生效
+
+# 方案B: 使用sudo
+sudo docker-compose up -d
+```
+
+---
+
+### 8. 内存不足 ⚠️
+
+**症状**: 容器频繁重启或OOM错误
+
+**解决方案**:
+```bash
+# 限制内存使用
+docker run -d \
+  --name sm2-sign-server \
+  -p 8888:8888 \
+  --memory="256m" \
+  --memory-swap="512m" \
+  sign-server:latest
+
+# 或修改docker-compose.yml
+services:
+  sign-server:
+    deploy:
+      resources:
+        limits:
+          memory: 256M
+```
+
+---
+
+### 9. 环境变量不生效 ⚠️
+
+**症状**: 传入的私钥未使用
+
+**检查**:
+```bash
+# 查看容器环境变量
+docker exec sm2-sign-server env | grep SM2
+```
+
+**正确设置方式**:
+
+**docker-compose.yml**:
+```yaml
+services:
+  sign-server:
+    environment:
+      SM2_PRIVATE_KEY: "您的私钥"
+      REQ_ORG_NO: "您的机构号"
+```
+
+**命令行**:
+```bash
+docker run -d \
+  -e SM2_PRIVATE_KEY="私钥" \
+  -e REQ_ORG_NO="机构号" \
+  sign-server:latest
+```
+
+---
+
+### 10. 配置文件挂载失败 ⚠️
+
+**错误**: 配置文件不生效
+
+**正确挂载方式**:
+```bash
+# 1. 创建配置文件
+cat > config.properties << EOF
+reqOrgNo=201811200001003
+priKey=您的私钥
+EOF
+
+# 2. 挂载到容器
+docker run -d \
+  -v $(pwd)/config.properties:/app/config.properties:ro \
+  sign-server:latest
+
+# 注意: 使用绝对路径或$(pwd)
+```
+
+**docker-compose.yml**:
+```yaml
+services:
+  sign-server:
+    volumes:
+      - ./config.properties:/app/config.properties:ro
+```
+
+---
+
+## 快速诊断命令
+
+```bash
+# 一键诊断脚本
+#!/bin/bash
+
+echo "=== Docker状态 ==="
+docker --version
+echo ""
+
+echo "=== 容器状态 ==="
+docker ps -a | grep sm2-sign-server
+echo ""
+
+echo "=== 端口占用 ==="
+lsof -i :8888
+echo ""
+
+echo "=== 容器日志(最后20行)==="
+docker logs --tail 20 sm2-sign-server
+echo ""
+
+echo "=== 网络测试 ==="
+curl -s http://localhost:8888/api/health || echo "❌ 服务不可访问"
+```
+
+---
+
+## 日志查看技巧
+
+```bash
+# 实时查看日志
+docker logs -f sm2-sign-server
+
+# 查看最近100行
+docker logs --tail 100 sm2-sign-server
+
+# 查看带时间戳的日志
+docker logs -t sm2-sign-server
+
+# 查看最近10分钟的日志
+docker logs --since 10m sm2-sign-server
+```
+
+---
+
+## 完全重置
+
+如果以上都不行,完全重置:
+
+```bash
+# 1. 停止并删除所有相关容器
+docker stop sm2-sign-server
+docker rm sm2-sign-server
+
+# 2. 删除镜像
+docker rmi sign-server:latest
+
+# 3. 清理缓存(可选)
+docker system prune -a
+
+# 4. 重新构建和启动
+docker-compose build --no-cache
+docker-compose up -d
+
+# 5. 验证
+docker logs -f sm2-sign-server
+```
+
+---
+
+## 获取帮助
+
+如果问题仍未解决:
+
+1. **查看完整日志**:
+   ```bash
+   docker logs sm2-sign-server > docker.log
+   cat docker.log
+   ```
+
+2. **检查系统资源**:
+   ```bash
+   df -h          # 磁盘空间
+   free -h        # 内存
+   docker stats   # 容器资源使用
+   ```
+
+3. **容器详细信息**:
+   ```bash
+   docker inspect sm2-sign-server
+   ```
+
+---
+
+## 常用管理命令速查
+
+```bash
+# 启动
+docker-compose up -d
+
+# 停止
+docker-compose down
+
+# 重启
+docker-compose restart
+
+# 查看状态
+docker-compose ps
+
+# 查看日志
+docker-compose logs -f
+
+# 重新构建
+docker-compose build --no-cache
+
+# 强制重启
+docker-compose down
+docker-compose up -d --force-recreate
+
+# 更新镜像
+docker-compose pull
+docker-compose up -d
+```
+
+---
+
+## 性能优化建议
+
+### 1. 使用更小的基础镜像
+
+```dockerfile
+# 当前使用 (85MB)
+FROM openjdk:8-jre-alpine
+
+# 备选方案 (更小)
+FROM adoptopenjdk:8-jre-hotspot-alpine
+```
+
+### 2. 限制日志大小
+
+```bash
+docker run -d \
+  --log-opt max-size=10m \
+  --log-opt max-file=3 \
+  sign-server:latest
+```
+
+### 3. 健康检查
+
+在Dockerfile添加:
+```dockerfile
+HEALTHCHECK --interval=30s --timeout=3s \
+  CMD wget --quiet --tries=1 --spider http://localhost:8888/api/health || exit 1
+```
+
+---
+
+## 最佳实践
+
+1. ✅ 使用docker-compose管理容器
+2. ✅ 通过环境变量传递敏感信息
+3. ✅ 设置合理的资源限制
+4. ✅ 配置日志轮转
+5. ✅ 启用自动重启(restart: unless-stopped)
+6. ✅ 定期更新基础镜像
+7. ✅ 使用健康检查
+8. ✅ 生产环境使用HTTPS
+
+---
+
+## 紧急恢复
+
+**服务完全失败时的快速恢复**:
+
+```bash
+#!/bin/bash
+# emergency-recovery.sh
+
+echo "开始紧急恢复..."
+
+# 停止所有
+docker-compose down
+docker rm -f sm2-sign-server
+
+# 清理
+docker system prune -f
+
+# 重建
+docker-compose build --no-cache
+
+# 启动
+docker-compose up -d
+
+# 等待
+sleep 5
+
+# 测试
+curl http://localhost:8888/api/health
+
+echo "恢复完成"
+```
+
+---
+
+**提示**: 将此文档保存为 `故障排除.md`,遇到问题时快速查阅。
+

+ 320 - 0
私钥配置说明.md

@@ -0,0 +1,320 @@
+# SM2私钥配置说明
+
+## 概述
+
+签名服务器支持多种方式配置私钥和公钥,您可以选择最适合您使用场景的方式。
+
+---
+
+## 配置方式(按优先级从高到低)
+
+### 1. 通过API接口传入(推荐)⭐
+
+**优点**:
+- 最灵活,每次请求可以使用不同的私钥
+- 支持多租户场景
+- 无需重启服务器
+- 私钥不需要存储在服务器上
+
+**缺点**:
+- 需要在每次请求中传递私钥
+- 需要客户端妥善保管私钥
+
+**使用方法**:
+
+签名时传入私钥:
+```bash
+curl -X POST http://localhost:8888/api/sign \
+  -H "Content-Type: application/json" \
+  -d '{
+    "data": {
+      "version": "1.0",
+      "txnType": "20250",
+      "reqOrgId": "201811200001003"
+    },
+    "priKey": "您的64位十六进制私钥",
+    "reqOrgNo": "您的机构号"
+  }'
+```
+
+验签时传入公钥:
+```bash
+curl -X POST http://localhost:8888/api/verify \
+  -H "Content-Type: application/json" \
+  -d '{
+    "data": {
+      "version": "1.0",
+      "txnType": "20250"
+    },
+    "sign": "签名字符串",
+    "pubKey": "您的130位十六进制公钥",
+    "reqOrgNo": "您的机构号"
+  }'
+```
+
+---
+
+### 2. 通过命令行参数
+
+**优点**:
+- 启动时指定,灵活方便
+- 适合脚本自动化部署
+
+**缺点**:
+- 私钥可能在进程列表中可见(不够安全)
+- 修改需要重启服务
+
+**使用方法**:
+
+```bash
+# 语法:
+java -cp "bin:lib/*" demo.com.SignServer [配置文件路径] [机构号] [私钥] [公钥]
+
+# 示例:
+java -cp "bin:lib/*" demo.com.SignServer \
+  "" \
+  "201811200001003" \
+  "3164EE0DF2BCA7A12309383E3305DD6563A28DFE53F65BBD60B3A1D7F80AC275" \
+  "046875695CDF1EF046ABB231FDAFA6DCA2AF1E5719EAC00DE80D65FEF03F8485DC9DCBBC10A9A46D565B4CDCEE3510F276209657CAE5BAC10C9678583A44F7F100"
+```
+
+---
+
+### 3. 通过环境变量(推荐用于生产环境)⭐
+
+**优点**:
+- 相对安全,私钥不会出现在代码或命令行中
+- 适合容器化部署(Docker、K8s)
+- 便于CI/CD集成
+
+**缺点**:
+- 修改需要重启服务
+
+**使用方法**:
+
+```bash
+# 设置环境变量
+export SM2_PRIVATE_KEY="您的64位十六进制私钥"
+export SLT_PUBLIC_KEY="您的130位十六进制公钥"
+export REQ_ORG_NO="您的机构号"
+
+# 启动服务
+java -cp "bin:lib/*" demo.com.SignServer
+```
+
+Docker示例:
+```bash
+docker run -d \
+  -e SM2_PRIVATE_KEY="您的私钥" \
+  -e SLT_PUBLIC_KEY="您的公钥" \
+  -e REQ_ORG_NO="您的机构号" \
+  -p 8888:8888 \
+  your-sign-server
+```
+
+---
+
+### 4. 通过配置文件
+
+**优点**:
+- 集中管理配置
+- 易于维护
+- 支持多个配置项
+
+**缺点**:
+- 私钥存储在文件中,需要注意文件权限
+- 修改需要重启服务
+
+**使用方法**:
+
+1. 创建配置文件 `config.properties`:
+```properties
+# 机构配置
+reqOrgNo=201811200001003
+
+# SM2私钥(64位十六进制)
+priKey=3164EE0DF2BCA7A12309383E3305DD6563A28DFE53F65BBD60B3A1D7F80AC275
+
+# 平台公钥(130位十六进制)
+sltPubKey=046875695CDF1EF046ABB231FDAFA6DCA2AF1E5719EAC00DE80D65FEF03F8485DC9DCBBC10A9A46D565B4CDCEE3510F276209657CAE5BAC10C9678583A44F7F100
+```
+
+2. 设置文件权限(重要!):
+```bash
+chmod 600 config.properties
+```
+
+3. 启动服务:
+```bash
+# 使用默认配置文件(config.properties)
+java -cp "bin:lib/*" demo.com.SignServer
+
+# 或指定配置文件路径
+java -cp "bin:lib/*" demo.com.SignServer /path/to/your/config.properties
+```
+
+---
+
+### 5. 代码中的默认配置
+
+**优点**:
+- 开箱即用,适合快速测试
+
+**缺点**:
+- 不安全,不适合生产环境
+- 私钥写死在代码中
+- 修改需要重新编译
+
+**使用方法**:
+
+修改 `SignServer.java` 文件:
+```java
+// 请求机构号
+private static String reqOrgNo = "您的机构号";
+// 请求机构的私钥
+private static String priKey = "您的SM2私钥";
+// 平台给商户的公钥
+private static String sltPubKey = "您的SM2公钥";
+```
+
+然后重新编译并启动。
+
+---
+
+## 配置优先级
+
+当多种配置方式同时存在时,优先级为:
+
+```
+API接口传入 > 命令行参数 > 环境变量 > 配置文件 > 代码默认值
+```
+
+**说明**:
+- 高优先级的配置会覆盖低优先级的配置
+- 如果某个参数在高优先级中未设置,则使用低优先级的值
+
+---
+
+## 使用场景建议
+
+### 场景1:开发测试
+**建议**: 使用 **API接口传入** 或 **配置文件**
+- 快速测试不同的密钥
+- 便于调试
+
+### 场景2:生产环境(单机部署)
+**建议**: 使用 **配置文件** + 严格的文件权限
+- 集中管理
+- 注意设置 `chmod 600` 保护配置文件
+
+### 场景3:生产环境(容器化)
+**建议**: 使用 **环境变量** 或 **Secret管理**
+- 与Kubernetes Secrets或Docker Secrets集成
+- 不将私钥写入镜像
+
+### 场景4:多租户SaaS服务
+**建议**: 使用 **API接口传入**
+- 每个租户使用自己的密钥
+- 服务器不存储任何私钥
+- 最安全的方案
+
+---
+
+## 安全建议
+
+### ⚠️ 重要安全提示
+
+1. **永远不要将私钥提交到代码仓库**
+   ```bash
+   # 添加到 .gitignore
+   config.properties
+   *.key
+   ```
+
+2. **使用配置文件时,严格限制文件权限**
+   ```bash
+   chmod 600 config.properties
+   chown app_user:app_group config.properties
+   ```
+
+3. **生产环境建议使用密钥管理服务**
+   - AWS KMS
+   - Azure Key Vault
+   - HashiCorp Vault
+
+4. **启用HTTPS**
+   - 如果通过API传递私钥,必须使用HTTPS
+   - 防止私钥在网络传输中被窃取
+
+5. **日志脱敏**
+   - 服务器日志中会隐藏私钥中间部分
+   - 格式:`3164EE0D...F80AC275`
+
+---
+
+## SM2密钥格式说明
+
+### 私钥格式
+- **长度**: 64位十六进制字符(32字节)
+- **示例**: `3164EE0DF2BCA7A12309383E3305DD6563A28DFE53F65BBD60B3A1D7F80AC275`
+- **字符集**: 0-9, A-F(大小写均可)
+
+### 公钥格式(非压缩)
+- **长度**: 130位十六进制字符(65字节)
+- **前缀**: 04(表示非压缩格式)
+- **示例**: `046875695CDF1EF046ABB231FDAFA6DCA2AF1E5719EAC00DE80D65FEF03F8485DC9DCBBC10A9A46D565B4CDCEE3510F276209657CAE5BAC10C9678583A44F7F100`
+
+---
+
+## 常见问题
+
+### Q1: 私钥格式错误怎么办?
+**A**: 确保私钥是64位的十六进制字符串,不包含空格、换行等其他字符。
+
+### Q2: 可以在一次请求中只传私钥,不传机构号吗?
+**A**: 可以,如果不传机构号,会使用服务器配置的默认机构号。
+
+### Q3: API传入的私钥会被服务器保存吗?
+**A**: 不会,私钥仅在当次请求中使用,请求结束后立即释放,不会持久化存储。
+
+### Q4: 多个客户端可以使用不同的私钥吗?
+**A**: 可以,通过API接口传入私钥的方式,每个客户端都可以使用自己的私钥。
+
+---
+
+## 快速测试
+
+启动服务器后,测试不同配置方式:
+
+```bash
+# 测试1:使用服务器默认配置
+curl -X POST http://localhost:8888/api/sign \
+  -H "Content-Type: application/json" \
+  -d '{"data": {"version": "1.0"}}'
+
+# 测试2:传入自定义私钥
+curl -X POST http://localhost:8888/api/sign \
+  -H "Content-Type: application/json" \
+  -d '{
+    "data": {"version": "1.0"},
+    "priKey": "3164EE0DF2BCA7A12309383E3305DD6563A28DFE53F65BBD60B3A1D7F80AC275",
+    "reqOrgNo": "TEST123"
+  }'
+```
+
+---
+
+## 总结
+
+选择合适的配置方式取决于您的具体需求:
+
+| 配置方式 | 灵活性 | 安全性 | 适用场景 |
+|---------|--------|--------|----------|
+| API传入 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | 多租户、SaaS |
+| 命令行参数 | ⭐⭐⭐⭐ | ⭐⭐ | 脚本部署 |
+| 环境变量 | ⭐⭐⭐ | ⭐⭐⭐⭐ | 容器化部署 |
+| 配置文件 | ⭐⭐ | ⭐⭐⭐ | 单机部署 |
+| 代码默认值 | ⭐ | ⭐ | 仅供测试 |
+
+**生产环境推荐**: API传入(多租户)或 环境变量(单租户)
+

+ 281 - 0
私钥错误修复说明.md

@@ -0,0 +1,281 @@
+# 私钥签名错误修复说明
+
+## 问题描述
+
+当使用自定义私钥调用签名接口时,出现以下错误:
+
+```json
+{
+  "error": "签名失败: The multiplicator cannot be negative",
+  "success": false
+}
+```
+
+###错误示例
+
+```bash
+curl -X POST http://localhost:8888/api/sign \
+  -H "Content-Type: application/json" \
+  -d '{
+    "data": {"version": "1.0"},
+    "priKey": "AABBCCDD0011223344556677889900AABBCCDD0011223344556677889900AA",
+    "reqOrgNo": "TEST123456"
+  }'
+
+# 返回错误
+{"error":"签名失败: The multiplicator cannot be negative","success":false}
+```
+
+---
+
+## 根本原因
+
+问题出在 `SM2Utils.java` 第135行:
+
+```java
+// 错误的代码
+BigInteger userD = new BigInteger(privateKey);
+```
+
+当使用 `new BigInteger(byte[])` 构造器时:
+- 如果字节数组的第一个字节 >= 128(即最高位为1)
+- Java会将其解释为**负数**(二进制补码表示)
+- 导致后续的椭圆曲线运算失败
+
+### 示例说明
+
+```java
+// 假设私钥第一个字节是 0xFA (250)
+byte[] key = Hex.decode("FAA43158512CCA4DEBCA3047F4BD6F9E...");
+
+// 错误方式:会被解释为负数
+BigInteger wrong = new BigInteger(key);  
+// wrong < 0  ❌
+
+// 正确方式:明确指定为正数
+BigInteger correct = new BigInteger(1, key);  
+// correct > 0  ✅
+```
+
+---
+
+## 解决方案
+
+### 修复代码
+
+在 `/Users/light/www/pros/xingfutong-java/src/demo/com/util/sm/SM2Utils.java` 第135行:
+
+**修改前**:
+```java
+BigInteger userD = new BigInteger(privateKey);
+```
+
+**修改后**:
+```java
+BigInteger userD = new BigInteger(1, privateKey);  // 1 表示正数符号位
+```
+
+### 完整修复步骤
+
+#### 步骤1: 修改代码
+
+代码已自动修复(参考上述修改)。
+
+#### 步骤2: 重新编译
+
+```bash
+cd /Users/light/www/pros/xingfutong-java
+
+# 编译SM2Utils
+javac -encoding UTF-8 -d bin -cp "lib/*:bin" \
+  src/demo/com/util/sm/SM2Utils.java
+```
+
+#### 步骤3: 重新构建Docker镜像
+
+```bash
+# 停止旧容器
+docker-compose down
+
+# 重新构建镜像
+docker-compose build --no-cache
+
+# 启动新容器
+docker-compose up -d
+```
+
+或使用一键脚本:
+```bash
+./deploy.sh
+```
+
+---
+
+## 验证修复
+
+### 生成有效的SM2密钥对
+
+```bash
+java -cp "bin:lib/*" demo.com.GenerateKeyPair
+```
+
+输出示例:
+```
+私钥 (64位十六进制):
+FAA43158512CCA4DEBCA3047F4BD6F9E3BF1C10685E5FFB80B34B48DA854DCC1
+
+公钥 (130位十六进制):
+049DC52B5FFB819C19DB9FA4DC1AF97B754810E9EBCFF85D83BDFAB452C5DD53E8...
+```
+
+### 测试签名功能
+
+```bash
+curl -X POST http://localhost:8888/api/sign \
+  -H "Content-Type: application/json" \
+  -d '{
+    "data": {
+      "version": "1.0",
+      "test": "hello"
+    },
+    "priKey": "FAA43158512CCA4DEBCA3047F4BD6F9E3BF1C10685E5FFB80B34B48DA854DCC1",
+    "reqOrgNo": "MY_ORG"
+  }'
+```
+
+**期望结果**(修复后):
+```json
+{
+  "success": true,
+  "signData": "TEST=hello|VERSION=1.0",
+  "sign": "3044022015A7C08F039EAFAD3330D9B4456BD21B...",
+  "timestamp": 1761292521531
+}
+```
+
+---
+
+## 为什么会出现这个问题?
+
+### Java BigInteger 的构造器说明
+
+Java的 `BigInteger` 类有多个构造器:
+
+```java
+// 1. 从字节数组构造(有符号)
+new BigInteger(byte[] val)
+// - 使用二进制补码表示
+// - 第一个字节的最高位是符号位
+// - 如果最高位=1,则为负数
+
+// 2. 从字节数组构造(指定符号)
+new BigInteger(int signum, byte[] magnitude)
+// - signum: -1负数, 0零, 1正数
+// - magnitude: 绝对值的二进制表示
+// - 总是按无符号解释字节数组
+```
+
+### SM2私钥的特点
+
+SM2私钥是32字节(256位)的随机数:
+- 范围:1 到 n-1(n是椭圆曲线的阶)
+- 十六进制表示:64个字符
+- **约50%的私钥第一个字节 >= 128**
+
+因此,如果不指定符号位,大约一半的私钥会被错误解释为负数!
+
+---
+
+## 其他说明
+
+### 1. 默认私钥为什么能工作?
+
+项目中的默认私钥:
+```
+3164EE0DF2BCA7A12309383E3305DD6563A28DFE53F65BBD60B3A1D7F80AC275
+```
+
+第一个字节是 `0x31` (49),小于 128,所以被正确解释为正数。
+
+### 2. 哪些私钥会出错?
+
+第一个字节 >= 128(80-FF)的私钥会出错,例如:
+- `FAA43158...` (0xFA = 250)
+- `AAB43158...` (0xAA = 170)
+- `80000000...` (0x80 = 128)
+
+第一个字节 < 128(00-7F)的私钥正常,例如:
+- `31640000...` (0x31 = 49)
+- `7FFFFFFF...` (0x7F = 127)
+
+### 3. 是否需要重新生成密钥?
+
+**不需要!** 修复后,所有有效的SM2私钥都能正常使用。
+
+---
+
+## 生成密钥对工具
+
+项目已包含密钥对生成工具 `GenerateKeyPair.java`。
+
+### 使用方法
+
+```bash
+# 生成1对密钥
+java -cp "bin:lib/*" demo.com.GenerateKeyPair
+
+# 生成5对密钥
+java -cp "bin:lib/*" demo.com.GenerateKeyPair 5
+```
+
+### 输出内容
+
+- 私钥(64位十六进制)
+- 公钥(130位十六进制)
+- 配置文件格式
+- API调用示例
+
+---
+
+## 安全提醒
+
+1. ⚠️ **私钥必须保密!**
+   - 私钥泄露意味着签名被破解
+   - 不要将私钥提交到代码仓库
+   - 生产环境使用环境变量或密钥管理服务
+
+2. ⚠️ **使用HTTPS!**
+   - 如果通过API传递私钥,必须使用HTTPS
+   - 防止中间人攻击窃取私钥
+
+3. ⚠️ **定期轮换密钥!**
+   - 定期更换密钥对
+   - 保留旧公钥用于验证历史签名
+
+---
+
+## 总结
+
+| 项目 | 说明 |
+|------|------|
+| **问题** | 部分私钥签名失败 |
+| **原因** | BigInteger构造器未指定正数符号位 |
+| **影响** | 约50%的随机私钥受影响 |
+| **修复** | 使用 `new BigInteger(1, privateKey)` |
+| **状态** | 已修复 ✅ |
+| **测试** | 需重新编译和部署 |
+
+---
+
+## 参考资料
+
+- Java BigInteger API文档
+- SM2国密标准 (GM/T 0003-2012)
+- BouncyCastle密码库文档
+
+---
+
+**最后更新**: 2025-10-24
+
+**修复作者**: AI Assistant
+

+ 447 - 0
部署说明.md

@@ -0,0 +1,447 @@
+# Docker部署说明
+
+## 前置要求
+
+服务器上只需要安装 **Docker** 即可,不需要Java环境!
+
+### 安装Docker(如果未安装)
+
+**CentOS/RHEL:**
+```bash
+curl -fsSL https://get.docker.com | sh
+systemctl start docker
+systemctl enable docker
+```
+
+**Ubuntu/Debian:**
+```bash
+curl -fsSL https://get.docker.com | sh
+systemctl start docker
+systemctl enable docker
+```
+
+**Docker版本检查:**
+```bash
+docker --version
+```
+
+---
+
+## 部署方式
+
+### 方式一:使用Dockerfile(推荐)⭐
+
+#### 1. 上传项目到服务器
+
+```bash
+# 方式1: 使用scp
+scp -r xingfutong-java/ user@your-server:/opt/
+
+# 方式2: 使用rsync
+rsync -avz xingfutong-java/ user@your-server:/opt/xingfutong-java/
+
+# 方式3: 使用git
+ssh user@your-server
+cd /opt
+git clone your-repo-url xingfutong-java
+```
+
+#### 2. 构建Docker镜像
+
+```bash
+cd /opt/xingfutong-java
+
+# 确保已经编译过(如果没有,在本地编译好再上传)
+# 本地编译命令:javac -encoding UTF-8 -d bin -cp "lib/*" $(find src -name "*.java")
+
+# 构建镜像
+docker build -t sign-server:latest .
+```
+
+#### 3. 运行容器
+
+**基础运行(使用代码中的默认配置):**
+```bash
+docker run -d \
+  --name sm2-sign-server \
+  -p 8888:8888 \
+  --restart unless-stopped \
+  sign-server:latest
+```
+
+**传入自定义私钥(通过环境变量):**
+```bash
+docker run -d \
+  --name sm2-sign-server \
+  -p 8888:8888 \
+  -e SM2_PRIVATE_KEY="您的64位十六进制私钥" \
+  -e SLT_PUBLIC_KEY="您的130位十六进制公钥" \
+  -e REQ_ORG_NO="您的机构号" \
+  --restart unless-stopped \
+  sign-server:latest
+```
+
+**使用配置文件:**
+```bash
+# 创建配置文件
+cat > config.properties << EOF
+reqOrgNo=201811200001003
+priKey=您的私钥
+sltPubKey=您的公钥
+EOF
+
+# 运行容器并挂载配置文件
+docker run -d \
+  --name sm2-sign-server \
+  -p 8888:8888 \
+  -v $(pwd)/config.properties:/app/config.properties:ro \
+  --restart unless-stopped \
+  sign-server:latest
+```
+
+---
+
+### 方式二:使用Docker Compose(更简单)⭐⭐
+
+#### 1. 上传项目到服务器(同上)
+
+#### 2. 启动服务(一条命令搞定)
+
+```bash
+cd /opt/xingfutong-java
+
+# 构建并启动
+docker-compose up -d
+
+# 查看日志
+docker-compose logs -f
+```
+
+#### 3. 停止/重启服务
+
+```bash
+# 停止
+docker-compose down
+
+# 重启
+docker-compose restart
+
+# 查看状态
+docker-compose ps
+```
+
+---
+
+## 管理命令
+
+### 查看容器状态
+```bash
+docker ps
+```
+
+### 查看日志
+```bash
+# 实时查看日志
+docker logs -f sm2-sign-server
+
+# 查看最近100行
+docker logs --tail 100 sm2-sign-server
+```
+
+### 停止容器
+```bash
+docker stop sm2-sign-server
+```
+
+### 启动容器
+```bash
+docker start sm2-sign-server
+```
+
+### 重启容器
+```bash
+docker restart sm2-sign-server
+```
+
+### 删除容器
+```bash
+docker stop sm2-sign-server
+docker rm sm2-sign-server
+```
+
+### 进入容器调试
+```bash
+docker exec -it sm2-sign-server sh
+```
+
+---
+
+## 更新部署
+
+### 方式1:重新构建镜像
+
+```bash
+cd /opt/xingfutong-java
+
+# 停止旧容器
+docker stop sm2-sign-server
+docker rm sm2-sign-server
+
+# 重新构建镜像
+docker build -t sign-server:latest .
+
+# 启动新容器
+docker run -d \
+  --name sm2-sign-server \
+  -p 8888:8888 \
+  --restart unless-stopped \
+  sign-server:latest
+```
+
+### 方式2:使用Docker Compose
+
+```bash
+cd /opt/xingfutong-java
+
+# 重新构建并启动
+docker-compose up -d --build
+```
+
+---
+
+## 端口和防火墙配置
+
+### 开放8888端口
+
+**CentOS/RHEL (firewalld):**
+```bash
+firewall-cmd --permanent --add-port=8888/tcp
+firewall-cmd --reload
+```
+
+**Ubuntu/Debian (ufw):**
+```bash
+ufw allow 8888/tcp
+ufw reload
+```
+
+**iptables:**
+```bash
+iptables -A INPUT -p tcp --dport 8888 -j ACCEPT
+service iptables save
+```
+
+### 修改端口
+
+如果想使用其他端口(比如80):
+
+```bash
+# 使用Docker端口映射
+docker run -d \
+  --name sm2-sign-server \
+  -p 80:8888 \
+  sign-server:latest
+
+# 访问时使用: http://your-server/api/sign
+```
+
+---
+
+## 测试部署
+
+```bash
+# 健康检查
+curl http://localhost:8888/api/health
+
+# 或从外部访问
+curl http://your-server-ip:8888/api/health
+
+# 测试签名接口
+curl -X POST http://localhost:8888/api/sign \
+  -H "Content-Type: application/json" \
+  -d '{
+    "data": {
+      "version": "1.0",
+      "txnType": "20250"
+    }
+  }'
+```
+
+---
+
+## 生产环境建议
+
+### 1. 使用Nginx反向代理(可选)
+
+```nginx
+# /etc/nginx/conf.d/sign-server.conf
+server {
+    listen 80;
+    server_name sign.yourdomain.com;
+
+    location / {
+        proxy_pass http://localhost:8888;
+        proxy_set_header Host $host;
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+        proxy_set_header X-Forwarded-Proto $scheme;
+    }
+}
+```
+
+### 2. 启用HTTPS(推荐)
+
+```bash
+# 使用Let's Encrypt
+apt install certbot python3-certbot-nginx
+certbot --nginx -d sign.yourdomain.com
+```
+
+### 3. 限制内存和CPU(可选)
+
+```bash
+docker run -d \
+  --name sm2-sign-server \
+  -p 8888:8888 \
+  --memory="512m" \
+  --cpus="1.0" \
+  --restart unless-stopped \
+  sign-server:latest
+```
+
+### 4. 日志管理
+
+```bash
+# 限制日志大小
+docker run -d \
+  --name sm2-sign-server \
+  -p 8888:8888 \
+  --log-opt max-size=10m \
+  --log-opt max-file=3 \
+  --restart unless-stopped \
+  sign-server:latest
+```
+
+---
+
+## 监控和告警
+
+### 使用Docker健康检查
+
+修改Dockerfile添加健康检查:
+```dockerfile
+HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
+  CMD wget --quiet --tries=1 --spider http://localhost:8888/api/health || exit 1
+```
+
+### 查看健康状态
+```bash
+docker ps
+# 状态列会显示健康状态
+```
+
+---
+
+## 常见问题
+
+### Q1: 容器启动失败?
+```bash
+# 查看详细日志
+docker logs sm2-sign-server
+
+# 检查端口是否被占用
+netstat -tlnp | grep 8888
+```
+
+### Q2: 无法访问服务?
+```bash
+# 检查容器是否运行
+docker ps -a
+
+# 检查防火墙
+firewall-cmd --list-ports
+```
+
+### Q3: 如何备份镜像?
+```bash
+# 导出镜像
+docker save sign-server:latest > sign-server.tar
+
+# 在其他服务器导入
+docker load < sign-server.tar
+```
+
+### Q4: 内存不足?
+使用Alpine Linux的JRE镜像(已使用),镜像大小仅约85MB。
+
+---
+
+## 一键部署脚本
+
+创建 `deploy.sh`:
+
+```bash
+#!/bin/bash
+
+echo "开始部署SM2签名服务..."
+
+# 检查Docker
+if ! command -v docker &> /dev/null; then
+    echo "错误: 未安装Docker,请先安装Docker"
+    exit 1
+fi
+
+# 停止旧容器
+docker stop sm2-sign-server 2>/dev/null
+docker rm sm2-sign-server 2>/dev/null
+
+# 构建镜像
+echo "构建Docker镜像..."
+docker build -t sign-server:latest .
+
+# 启动容器
+echo "启动容器..."
+docker run -d \
+  --name sm2-sign-server \
+  -p 8888:8888 \
+  --restart unless-stopped \
+  sign-server:latest
+
+# 等待启动
+sleep 3
+
+# 健康检查
+echo "检查服务状态..."
+curl -s http://localhost:8888/api/health
+
+echo ""
+echo "部署完成!"
+echo "访问地址: http://$(hostname -I | awk '{print $1}'):8888"
+```
+
+使用方法:
+```bash
+chmod +x deploy.sh
+./deploy.sh
+```
+
+---
+
+## 总结
+
+**最简单的部署流程**:
+
+1. 服务器安装Docker
+2. 上传项目文件到服务器
+3. 执行 `docker-compose up -d`
+4. 完成!
+
+**优势**:
+- ✅ 无需安装Java
+- ✅ 环境隔离,不影响其他服务
+- ✅ 一条命令启动
+- ✅ 自动重启
+- ✅ 易于扩展和迁移
+
+**镜像大小**: 约85MB(使用Alpine Linux)
+