Android接入火山引擎API,实现接口验签适配绝大部分接口

Android接入火山引擎API,实现接口验签适配绝大部分接口

需求背景

小成本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 queryList, byte[] body,

Date date) throws Exception {

// 请求头

Map 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 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 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 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,

var image_urls: ArrayList,

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) : Serializable

接口回调NormalCallBack

interface NormalCallBack {

fun onSuccess(any: Any)

fun onFail()

}

更多尼泊尔内容

小二体验记 使用秘籍必备
365体育亚洲官方登录

小二体验记 使用秘籍必备

🗓️ 09-03 👁️ 7408
项目管理pdm图怎么画
365体育亚洲官方登录

项目管理pdm图怎么画

🗓️ 08-12 👁️ 5057
墳塋的解释
38365365.com打不开

墳塋的解释

🗓️ 07-18 👁️ 4884
煅石之轮
38365365.com打不开

煅石之轮

🗓️ 08-07 👁️ 8783
手机通讯录备份全攻略:轻松保存你的联系人信息
365体育亚洲官方登录

手机通讯录备份全攻略:轻松保存你的联系人信息

🗓️ 07-01 👁️ 354
篁片在哪里找?详解篁片获取途径
38365365.com打不开

篁片在哪里找?详解篁片获取途径

🗓️ 08-15 👁️ 1425
中国父母为何恨游戏?
365体育推荐

中国父母为何恨游戏?

🗓️ 06-29 👁️ 8385
跳跳妹妹哪里多
38365365.com打不开

跳跳妹妹哪里多

🗓️ 07-02 👁️ 5947
顺丰快递寄狗狗多少钱?了解费用和注意事项,助你顺利寄送宠物