需求背景
小成本app开发,无后端服务器接入或者仅需简单调用单个api时,需要在android端直接实现接口调用,而火山引擎官方并未单独对android做适配,官方也有对应的sdk和文档,不过也没有供android使用的最新sdk,所以要实现api调用那么就需要本地自己做封装。
其实好像也有android能使用的sdk,如https://github.com/volcengine/volc-sdk-java 中的SNAPSHOT版本,目前最新是2.0.6-SNAPSHOT,对android做了适配,使用了okhttp可以兼容android,不过其中植入的接口都是比较老旧,但我看版本更新日期还是蛮新的,就是不知道为什么里面只有一些老api,并不适合我们使用。
而且火山的api调用目前接口名大部分都是一样的,区别就是携带参数,通过不同的参数来判断实现的api,而这些api均需要实现签名验证,所以这就是本篇文章需要实现的内容,只要实现了接口验签,那么业务需要的API调用仅仅是参数做相关修改而已,如果不需要参数验签的话那就更好做了,简单的api调用通过android实现并不难。
本文主要使用java,部分代码使用kotlin编写,主要通过他们官方java文档修改过来的,所以没做太多更改
签名验证
https://www.volcengine.com/docs/6369/67268
火山官方文档说明了签名的参数和实现方法,而且还有签名过程demo,在我们使用可能有不一样的地方,所以这里用java版本的demo代码做了一些修改来实现的方案,下面直接贴上完整代码。
Signer类便是签名的实现类,在调用前通过创建signer类的实例初始化需要的参数,然后通过调用calcAuthorization方法实现签名验签并且把签名相关数据组合成Headers请求头返回,再在需要的地方组合请求。
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.BitSet;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.SortedMap;
import java.util.TimeZone;
import java.util.TreeMap;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import okhttp3.Headers;
public class Signer {
private static final BitSet URLENCODER = new BitSet(256);
private static final String CONST_ENCODE = "0123456789ABCDEF";
public static final Charset UTF_8 = StandardCharsets.UTF_8;
private final String region;
private final String service;
private final String host;
private final String path;
private final String ak;
private final String sk;
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('~');
}
public Signer(String region, String service, String host, String path, String ak, String sk) {
this.region = region;
this.service = service;
this.host = host;
this.path = path;
this.ak = ak;
this.sk = sk;
}
public Headers calcAuthorization(String method, Map<String, String> queryList, byte[] body,
Date date) throws Exception {
// 请求头
Map<String, String> headerMap = new HashMap<>();
String contentType = "application/json; charset=utf-8";
if (body == null) {
body = new byte[0];
} else {
contentType = "application/json; charset=utf-8";
}
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 signHeader = "content-type;host;x-content-sha256;x-date";
// String signHeader = "content-type;host;x-date";
SortedMap<String, String> realQueryList = new TreeMap<>(queryList);
// 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" + path + "\n" + querySB + "\n" +
"content-type:" + contentType + "\n" +
"host:" + host + "\n" +
"x-content-sha256:" + xContentSha256 + "\n" +
"x-date:" + xDate + "\n" +
"\n" +
signHeader + "\n" +
xContentSha256;
// log.info("canonicalStringBuilder is {}", canonicalStringBuilder);
String hashcanonicalString = hashSHA256(canonicalStringBuilder.getBytes());
String credentialScope = shortXDate + "/" + region + "/" + service + "/request";
String signString = "HMAC-SHA256" + "\n" + xDate + "\n" + credentialScope + "\n" + hashcanonicalString;
// log.info("signString is {}", signString);
byte[] signKey = genSigningSecretKeyV4(sk, shortXDate, region, service);
String signature = bytesToHex(hmacSHA256(signKey, signString));
String auth = "HMAC-SHA256" +
" Credential=" + ak + "/" + credentialScope +
", SignedHeaders=" + signHeader +
", Signature=" + signature;
headerMap.put("Authorization", auth);
headerMap.put("X-Date", xDate);
headerMap.put("X-Content-Sha256", xContentSha256);
headerMap.put("Host", host);
headerMap.put("Content-Type", contentType);
headerMap.put("User-Agent", "volc-sdk-java/v");
headerMap.put("Accept", "application/json");
return Headers.of(headerMap);
}
public static 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 HexFormat.of().formatHex(md.digest(content));
return bytesToHex(md.digest(content));
} catch (Exception e) {
throw new Exception(
"Unable to compute hash while signing request: "
+ e.getMessage(), e);
}
}
public static String bytesToHex(byte[] bytes) {
StringBuilder hexString = new StringBuilder();
for (byte b : bytes) {
String hex = Integer.toHexString(0xFF & b);
if (hex.length() == 1) {
// 如果是一位的话,要补0
hexString.append('0');
}
hexString.append(hex);
}
return hexString.toString();
}
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");
}
}
接口调用
这里我用火山引擎人像人体中的年龄变化API来做示例,使用OKHttp实现网络请求,并封装OkUtil工具类实现火山API调用请求。
年龄变化api能实现上传一张人脸图片并设置年龄后返回该人脸对应年龄的AI生成图,目前年龄仅能选择5岁和70岁,图片上传和返回方式均使用的base64方式,通过调用OkUtil的huoShanAge方法即可实现接口调用
import static com.sugoilab.picture.utill.Signer.signStringEncoder;
import android.util.Log;
import com.blankj.utilcode.util.ConvertUtils;
import com.blankj.utilcode.util.GsonUtils;
import com.sugoilab.common.callback.NormalCallBack;
import com.sugoilab.common.util.JsonUtil;
import com.sugoilab.picture.bean.HuoShanAgeRequestBean;
import com.sugoilab.picture.bean.HuoShanAgeResponseBean;
import okhttp3.*;
import java.util.ArrayList;
import java.util.Date;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.concurrent.TimeUnit;
public class OkUtil {
private static final String TAG = "OkUtil";
private static final MediaType JSON_MEDIA_TYPE = MediaType.parse("application/json; charset=utf-8");
private static final int MAX_REQUESTS = 4;
private static final int CONNECTION_POOL_SIZE = 10;
private static final int CONNECTION_POOL_KEEP_ALIVE_DURATION = 5;
private static final int TIMEOUT_DURATION = 30;
private static volatile OkUtil instance;
private final OkHttpClient httpClient;
private OkUtil() {
this.httpClient = createHttpClient();
}
public static OkUtil getInstance() {
if (instance == null) {
synchronized (OkUtil.class) {
if (instance == null) {
instance = new OkUtil();
}
}
}
return instance;
}
private static OkHttpClient createHttpClient() {
Dispatcher dispatcher = new Dispatcher();
dispatcher.setMaxRequests(MAX_REQUESTS);
ConnectionPool connectionPool = new ConnectionPool(CONNECTION_POOL_SIZE, CONNECTION_POOL_KEEP_ALIVE_DURATION, TimeUnit.MINUTES);
return new OkHttpClient.Builder()
.dispatcher(dispatcher)
.connectionPool(connectionPool)
.connectTimeout(TIMEOUT_DURATION, TimeUnit.SECONDS)
.writeTimeout(TIMEOUT_DURATION, TimeUnit.SECONDS)
.readTimeout(TIMEOUT_DURATION, TimeUnit.SECONDS)
.build();
}
// 发送GET请求
// public void sendGetRequest(String url) {
// Request request = new Request.Builder()
// .url(url)
// .get()
// .build();
// try (Response response = httpClient.newCall(request).execute()) {
// if (response.isSuccessful()) {
// String responseBody = response.body().string();
// Log.d(TAG, "Response: " + responseBody);
// } else {
// Log.e(TAG, "Request failed with code: " + response.code());
// }
// } catch (Exception e) {
// Log.e(TAG, "Network request error", e);
// }
// }
// 发送POST请求
public void huoShanAge(int age, String base64, NormalCallBack callBack) {
//签名初始化参数
Signer signer = new Signer(
"cn-north-1",
"cv",
"visual.volcengineapi.com",
"/",
"AKLTYzU1ZDBiOWMyZTc4NGQxMDg3NzY1NDAxODRmN2Q2ODI",
"T0dVME1qWm1OMkV6WVdFek5ETXhObUkzTjJKaE5HSXlaVFkzT1dObU1EVQ=="
);
//query参数
SortedMap<String, String> realQueryList = new TreeMap<>();
realQueryList.put("Action", "AllAgeGeneration");
realQueryList.put("Version", "2022-08-31");
//query字段拼接
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);
//body参数
ArrayList<String> imgBase64 = new ArrayList<>();
imgBase64.add(base64);
HuoShanAgeRequestBean requestBean =
new HuoShanAgeRequestBean(
"all_age_generation",
imgBase64,
new ArrayList<>(),
age);
RequestBody requestBody = RequestBody.create(JSON_MEDIA_TYPE, JsonUtil.GsonString(requestBean));
Headers headers = null;
try {
//签名验签生成headers请求头
headers = signer.calcAuthorization("POST", realQueryList, ConvertUtils.string2Bytes(JsonUtil.GsonString(requestBean)), new Date());
} catch (Exception e) {
e.printStackTrace();
callBack.onFail();
}
//组合请求
Request request = new Request.Builder()
.url("https://visual.volcengineapi.com/?" + querySB)
.headers(headers)
.post(requestBody)
.build();
//请求发起
try (Response response = httpClient.newCall(request).execute()) {
String responseBody = response.body().string();
Log.d(TAG, "Response: " + responseBody);
callBack.onSuccess(GsonUtils.fromJson(responseBody, HuoShanAgeResponseBean.class));
} catch (Exception e) {
Log.e(TAG, "Network request error", e);
callBack.onFail();
}
}
}
上面工具类中使用到的数据类HuoShanAgeRequestBean和HuoShanAgeResponseBean代表请求需要上传的body数据和接口返回的数据,使用不同的api需要传入不同的参数和返回不同的结果,那就需要按照使用场景自行对数据类进行修改和应用
import java.io.Serializable
data class HuoShanAgeRequestBean(
var req_key: String,
var binary_data_base64: ArrayList<String>,
var image_urls: ArrayList<String>,
var target_age: Int
):Serializable
data class HuoShanAgeResponseBean(
var code: Int,
var data: HuoShanAgeDataResultBean?,
var message: String,
var request_id: String,
var status: Int,
var time_elapsed: String
) : Serializable
data class HuoShanAgeDataResultBean(var binary_data_base64: MutableList<String>) : Serializable
接口回调NormalCallBack
interface NormalCallBack {
fun onSuccess(any: Any)
fun onFail()
}