|
|
@@ -0,0 +1,209 @@
|
|
|
+package com.fs.aiSoundReplication.client;
|
|
|
+
|
|
|
+import com.fs.aiSoundReplication.config.VolcEngineSpeechConfig;
|
|
|
+import com.fasterxml.jackson.databind.ObjectMapper;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+import org.springframework.beans.factory.annotation.Autowired;
|
|
|
+import org.springframework.stereotype.Component;
|
|
|
+
|
|
|
+import javax.crypto.Mac;
|
|
|
+import javax.crypto.spec.SecretKeySpec;
|
|
|
+import java.io.DataInputStream;
|
|
|
+import java.io.InputStream;
|
|
|
+import java.io.OutputStream;
|
|
|
+import java.net.HttpURLConnection;
|
|
|
+import java.net.URL;
|
|
|
+import java.nio.ByteBuffer;
|
|
|
+import java.nio.charset.Charset;
|
|
|
+import java.nio.charset.StandardCharsets;
|
|
|
+import java.security.MessageDigest;
|
|
|
+import java.text.SimpleDateFormat;
|
|
|
+import java.util.*;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 火山引擎API客户端 - 负责HTTP请求发送和签名验证
|
|
|
+ */
|
|
|
+@Slf4j
|
|
|
+@Component
|
|
|
+public class VolcEngineApiClient {
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private VolcEngineSpeechConfig config;
|
|
|
+
|
|
|
+ private static final BitSet URLENCODER = new BitSet(256);
|
|
|
+ private static final String CONST_ENCODE = "0123456789abcdef";
|
|
|
+ public static final Charset UTF_8 = StandardCharsets.UTF_8;
|
|
|
+
|
|
|
+ static {
|
|
|
+ int i;
|
|
|
+ for (i = 97; i <= 122; ++i) {
|
|
|
+ URLENCODER.set(i);
|
|
|
+ }
|
|
|
+
|
|
|
+ for (i = 65; i <= 90; ++i) {
|
|
|
+ URLENCODER.set(i);
|
|
|
+ }
|
|
|
+
|
|
|
+ for (i = 48; i <= 57; ++i) {
|
|
|
+ URLENCODER.set(i);
|
|
|
+ }
|
|
|
+ URLENCODER.set('-');
|
|
|
+ URLENCODER.set('_');
|
|
|
+ URLENCODER.set('.');
|
|
|
+ URLENCODER.set('~');
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 执行API请求
|
|
|
+ */
|
|
|
+ public String doRequest(String method, Object requestBody,
|
|
|
+ Date date, String action, String version) throws Exception {
|
|
|
+ ObjectMapper objectMapper = new ObjectMapper();
|
|
|
+ String bodyJson = objectMapper.writeValueAsString(requestBody);
|
|
|
+ byte[] body = bodyJson.getBytes();
|
|
|
+
|
|
|
+ String xContentSha256 = hashSHA256(body);
|
|
|
+ SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'");
|
|
|
+ sdf.setTimeZone(TimeZone.getTimeZone("GMT"));
|
|
|
+ String xDate = sdf.format(date);
|
|
|
+ String shortXDate = xDate.substring(0, 8);
|
|
|
+ String contentType = "application/json; charset=utf-8";
|
|
|
+ String signHeader = "content-type;host;x-content-sha256;x-date";
|
|
|
+ SortedMap<String, String> realQueryList = new TreeMap<>();
|
|
|
+ realQueryList.put("Action", action);
|
|
|
+ realQueryList.put("Version", version);
|
|
|
+ StringBuilder querySB = new StringBuilder();
|
|
|
+ for (String key : realQueryList.keySet()) {
|
|
|
+ querySB.append(signStringEncoder(key)).append("=").append(signStringEncoder(realQueryList.get(key))).append("&");
|
|
|
+ }
|
|
|
+ querySB.deleteCharAt(querySB.length() - 1);
|
|
|
+
|
|
|
+ String canonicalStringBuilder = method + "\n" + config.getPath() + "\n" + querySB + "\n" +
|
|
|
+ "content-type:" + contentType + "\n" +
|
|
|
+ "host:" + config.getEndpoint() + "\n" +
|
|
|
+ "x-content-sha256:" + xContentSha256 + "\n" +
|
|
|
+ "x-date:" + xDate + "\n" +
|
|
|
+ "\n" +
|
|
|
+ signHeader + "\n" +
|
|
|
+ xContentSha256;
|
|
|
+
|
|
|
+ log.debug("Canonical Request: {}", canonicalStringBuilder);
|
|
|
+
|
|
|
+ String hashcanonicalString = hashSHA256(canonicalStringBuilder.getBytes());
|
|
|
+ String credentialScope = shortXDate + "/" + config.getRegion() + "/" + config.getService() + "/request";
|
|
|
+ String signString = "HMAC-SHA256" + "\n" + xDate + "\n" + credentialScope + "\n" + hashcanonicalString;
|
|
|
+
|
|
|
+ byte[] signKey = genSigningSecretKeyV4(config.getSecretAccessKey(), shortXDate, config.getRegion(), config.getService());
|
|
|
+ String signature = bytesToHex(hmacSHA256(signKey, signString));
|
|
|
+ log.debug("Signature: {}", signature);
|
|
|
+
|
|
|
+ String urlString = config.getSchema() + "://" + config.getEndpoint() + config.getPath() + "?" + querySB;
|
|
|
+ log.info("Request URL: {}", urlString);
|
|
|
+
|
|
|
+ URL url = new URL(urlString);
|
|
|
+
|
|
|
+ HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
|
|
+ conn.setRequestMethod(method);
|
|
|
+ conn.setRequestProperty("Host", config.getEndpoint());
|
|
|
+ conn.setRequestProperty("X-Date", xDate);
|
|
|
+ conn.setRequestProperty("X-Content-Sha256", xContentSha256);
|
|
|
+ conn.setRequestProperty("Content-Type", contentType);
|
|
|
+ conn.setRequestProperty("Authorization", "HMAC-SHA256" +
|
|
|
+ " Credential=" + config.getAccessKeyId() + "/" + credentialScope +
|
|
|
+ ", SignedHeaders=" + signHeader +
|
|
|
+ ", Signature=" + signature);
|
|
|
+
|
|
|
+ if (!Objects.equals(conn.getRequestMethod(), "GET")) {
|
|
|
+ conn.setDoOutput(true);
|
|
|
+ OutputStream os = conn.getOutputStream();
|
|
|
+ os.write(body);
|
|
|
+ os.flush();
|
|
|
+ os.close();
|
|
|
+ }
|
|
|
+ conn.connect();
|
|
|
+
|
|
|
+ int responseCode = conn.getResponseCode();
|
|
|
+
|
|
|
+ InputStream is;
|
|
|
+ if (responseCode == 200) {
|
|
|
+ is = conn.getInputStream();
|
|
|
+ } else {
|
|
|
+ is = conn.getErrorStream();
|
|
|
+ }
|
|
|
+ byte[] bytes = new byte[is.available()];
|
|
|
+ DataInputStream dataInputStream = new DataInputStream(is);
|
|
|
+ dataInputStream.readFully(bytes);
|
|
|
+ String responseBody = new String(bytes);
|
|
|
+ is.close();
|
|
|
+
|
|
|
+ log.info("Response Code: {}", responseCode);
|
|
|
+ log.debug("Response Body: {}", responseBody);
|
|
|
+
|
|
|
+ return responseBody;
|
|
|
+ }
|
|
|
+
|
|
|
+ private String signStringEncoder(String source) {
|
|
|
+ if (source == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ StringBuilder buf = new StringBuilder(source.length());
|
|
|
+ ByteBuffer bb = UTF_8.encode(source);
|
|
|
+ while (bb.hasRemaining()) {
|
|
|
+ int b = bb.get() & 255;
|
|
|
+ if (URLENCODER.get(b)) {
|
|
|
+ buf.append((char) b);
|
|
|
+ } else if (b == 32) {
|
|
|
+ buf.append("%20");
|
|
|
+ } else {
|
|
|
+ buf.append("%");
|
|
|
+ char hex1 = CONST_ENCODE.charAt(b >> 4);
|
|
|
+ char hex2 = CONST_ENCODE.charAt(b & 15);
|
|
|
+ buf.append(hex1);
|
|
|
+ buf.append(hex2);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return buf.toString();
|
|
|
+ }
|
|
|
+
|
|
|
+ public static String hashSHA256(byte[] content) throws Exception {
|
|
|
+ try {
|
|
|
+ MessageDigest md = MessageDigest.getInstance("SHA-256");
|
|
|
+
|
|
|
+ return bytesToHex(md.digest(content));
|
|
|
+ } catch (Exception e) {
|
|
|
+ throw new Exception(
|
|
|
+ "Unable to compute hash while signing request: "
|
|
|
+ + e.getMessage(), e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public static byte[] hmacSHA256(byte[] key, String content) throws Exception {
|
|
|
+ try {
|
|
|
+ Mac mac = Mac.getInstance("HmacSHA256");
|
|
|
+ mac.init(new SecretKeySpec(key, "HmacSHA256"));
|
|
|
+ return mac.doFinal(content.getBytes());
|
|
|
+ } catch (Exception e) {
|
|
|
+ throw new Exception(
|
|
|
+ "Unable to calculate a request signature: "
|
|
|
+ + e.getMessage(), e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private byte[] genSigningSecretKeyV4(String secretKey, String date, String region, String service) throws Exception {
|
|
|
+ byte[] kDate = hmacSHA256((secretKey).getBytes(), date);
|
|
|
+ byte[] kRegion = hmacSHA256(kDate, region);
|
|
|
+ byte[] kService = hmacSHA256(kRegion, service);
|
|
|
+ return hmacSHA256(kService, "request");
|
|
|
+ }
|
|
|
+
|
|
|
+ public static String bytesToHex(byte[] bytes) {
|
|
|
+ char[] hexChars = new char[bytes.length * 2];
|
|
|
+ for (int j = 0; j < bytes.length; j++) {
|
|
|
+ int v = bytes[j] & 0xFF;
|
|
|
+ hexChars[j * 2] = CONST_ENCODE.toCharArray()[v >>> 4];
|
|
|
+ hexChars[j * 2 + 1] = CONST_ENCODE.toCharArray()[v & 0x0F];
|
|
|
+ }
|
|
|
+ return new String(hexChars);
|
|
|
+ }
|
|
|
+}
|