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<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方式,通过调用OkUtilhuoShanAge方法即可实现接口调用


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();
        }
    }
}

上面工具类中使用到的数据类HuoShanAgeRequestBeanHuoShanAgeResponseBean代表请求需要上传的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()
}
### 火山引擎 API 调用 CLI 使用教程 #### 安装火山引擎 CLI 工具 要使用火山引擎的命令行接口 (CLI),首先需要安装对应的工具。通常可以通过包管理器来完成这一操作: 对于 macOS 和 Linux 用户,可以使用如下命令通过 Homebrew 或者其他包管理器进行安装: ```bash brew install volcengine-cli ``` Windows 用户可以从官方页面下载可执行文件并按照说明配置环境变量。 #### 配置认证信息 成功安装后,在首次使用前需设置访问凭证以证身份。这一步骤可通过 `volc config` 命令实现,会提示输入 Access Key ID 及 Secret Access Key 这两个用于鉴权的关键参数[^1]。 #### 查看可用服务列表 了解当前支持的服务及其端点地址有助于更好地构建请求。利用下面这条指令获取最新发布的各项功能概览: ```bash volc service list ``` 此命令返回的结果包含了各个产品的名称以及其对应的域名等重要详情。 #### 发送 HTTP 请求至指定 API 接口 假设想要调用某个特定的产品API,则可以根据官方文档提供的指导构造相应的 GET/POST 请求体,并借助通用形式发送数据给服务器端处理。例如向对象存储上传文件的操作可能涉及这样的流程: ```bash volc object put --bucket my-bucket-name --key path/to/file.txt --file /local/path/to/upload.txt ``` 上述例子展示了如何把本地的一个文本档推送至云端仓库内指定位置保存起来。 #### 获取帮助手册 如果遇到不确定的地方或是希望进一步探索更多高级特性的话,随时都可以求助于内置的帮助系统。只需键入 `volc help` 即能浏览到详尽的功能介绍与范例解析。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值