|
|
@@ -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;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|