首页 > 其他分享 >我发现了字节OpenApi接口的bug!

我发现了字节OpenApi接口的bug!

时间:2024-05-16 17:18:40浏览次数:21  
标签:String 示例 接口 OpenApi 文档 new bug 字节

本文记录我在对接字节旗下产品火山云旗下云游戏产品 OpenApi 接口文档时遇到的坑,希望能帮助大家(火山云旗下云游戏产品的文档坑很多,我算是从零到一都踩了一遍,特此记录,希望大家引以为鉴)。

1. 文档问题

很经典的开局一张图,对接全靠问,

image

image

这里给大家强调下,当要跟第三方产品对接时,一定要确认拿到的文档是不是最新版本。

比如我在这次对接中,第一次拿到的文档是产品给的,在业务中需要用到一个用户主动退出游戏的接口,于是我在第一份文档里面找到一个用户退出游戏的接口 RomoveUser。

image

但是当我在控制台调用此接口报错后,去群里一问才发现,对方建议我使用官网公布的最新接口文档。

官网最新文档:https://www.volcengine.com/docs/6512/143674

进入官网发现 RemoveUser 这个接口已经是历史接口了,官方建议换到 BanRoomUser 接口。

image

OK,这里算是踩到了第一个坑,文档版本不是最新。

ps:还要说一下,火山云旗下云游戏的这个 OpenApi 接口文档需要在群里联系他们开白才能看到,说实话给我的感觉很奇怪,怀疑产品是否有赶鸭子上架问题,暂且怀疑他们的目的是防止不明攻击吧。

2. OpenApi 示例 demo

第三方接口的接入一般都需要做鉴权。火山云旗下云游戏产品的 OpenApi 接口接入当然也不例外。于是我开始了第二个踩坑之旅,那就是他们给出的 OpenApi 示例 demo 的使用过于简单。

image

火山云旗下云游戏产品的 OpenApi 示例 demo 写的很简单,只提供了一个 GET 请求示例。

OpenApi 示例 demo 地址:https://github.com/volcengine/veGame

但是在我司的业务场景还是上个问题,需要一个用户主动退出游戏的接口,在火山云官网的 OpenApi 文档中我也找到了这个接口,就是上文提到的 BanRoomUser 接口。

但是在官方文档中 BanRoomUser 接口是一个 POST JSON 格式的请求。官方给出的 OpenApi 示例 demo 中并没有关于 POST JSON 请求的示例代码,所以只能靠我一个人查看他们提供的 SDK 依赖源码硬猜来写...,这就很让人头痛了。

好在我翻阅他们 SDK 源码中找到一个靠谱的 json(...) 请求方法,来完成这个 POST JSON 请求。

image

OK,说干就干,直接写好示例代码,开始发送 POST JSON 请求,

image

what f**k?什么鬼,返回了我一个 null,此时我的内心中充满了一个大大的问号。

我开始怀疑我的代码是不是写错了。但是当我经历过数次源码 debug 以及调用其他 OpenApi 接口测试并得到正确返回后,我坚定的认为我没错,这就是火山云 OpenApi 的 bug!

image

OK,说干就干,直接反馈给火山那边。

image

接着火山那边的人就联系说下午两点开会一起远程共享我的屏幕看看,OK 欣然接收,让他们见证下他们写的 bug!

...

时间来到下午两点,当我共享屏幕给字节工程师演示这个 bug 时,我的控制台打印如下,

image

woca,竟然不是 null!好在我脑袋灵活,思路清晰,瞬间想到我改了一个参数 GameId,之前返回 null 时,我传的 GameId 是一个假数据,现在我传的是一个真数据。造成了返回不一致。

OK,找到了返回正常的原因,当我把 GameId 改成假数据时,如我所愿,返回了一个 null。

image

自此,我也就在字节工程师的围观下,复现了他们的 OpenApi 接口的线上 bug。大功告成。

3. 鉴权失败

字节提供的 OpenApi 示例 demo 现在算是跑通了,但是由于我司项目一些依赖限制问题,我们不能直接引入火山云旗下云游戏产品的 SDK 依赖。所以我还得手动编写生成签名的代码。于是我开始了第三个踩坑之旅,那就是 GET 请求验签成功 POST 请求验签失败的问题。

这里先说一下,火山云提供了手动生成签名的示例代码

image

Java 生成签名的代码:https://github.com/volcengine/volc-openapi-demos/blob/main/signature/java/Sign.java

这里我也是直接把签名代码拿来即用就行,一开始接入生成签名代码非常顺利,GET 请求的 OpenApi 接口都是可以顺利调通的,但是当我调用 BanRoomUser 接口时(没错,又是这个接口,踩的三个坑都与这个接口有关),直接提示验签失败!

image

OK,开始排查为什么签名失败。

image

查看源码发现,POST JSON 请求时的 contentType 还是 application/x-www-form-urlencoded,直觉告诉我这里不对,所以改成 application/json 试试,看看控制台返回,

image

很好,还是验签失败!!!

我尽力了兄弟们,这个坑踩的我是无话可说。直接联系直接字节开发人员看下我的请求内容是哪里有问题。

在与字节开发人员一起观摩我写的代码以及生成的签名之后,大家都没找到问题所在。那没办法了,只能上服务器看接口请求日志了。

image

大家可以看出问题在哪里吗?没错我刚刚不是把 contentType 改成了 application/json 吗,为什么日志显示的 contentType 是 application/json; charset=utf-8!。

OK,到这里问题也找到了,原因是我这个项目用的 http 请求工具是 okhttp3。他自动给我拼接上去的!

那么怎么解决嘞,替换 http3 工具的话,改造成本比较大,所以我就顺势把代码的 contentType 也改成
application/json; charset=utf-8

在测试一遍,看看控制台打印。

image

OK,拿到成功响应,自此也就解决了第三个坑,POST JSON 请求时的验签不匹配问题。

最后给大家贴出手动生成验签的代码,有需要自取。

@Slf4j
public class Sign {
    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 Sign(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, String action, String version) throws Exception {
        // 请求头
        Map<String, String> headerMap = new HashMap<>();
        String contentType = "application/x-www-form-urlencoded; 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 = "20240515T061353Z";
        String xDate = sdf.format(date);
        String shortXDate = xDate.substring(0, 8);
        String signHeader = "content-type;host;x-content-sha256;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 = HexUtil.encodeHexStr(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);
    }

    private 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 HexUtil.encodeHexStr(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");
    }
}

总结

在与火山云旗下云游戏产品的 OpenApi 接口对接过程中,我总共踩了三个坑。一是文档版本不是最新,二是官方提供的 OpenApi 示例 demo 过于简单,三是官方提供的验签代码没有考虑到 POST JSON 请求场景下的 contentType 设置问题。

在这里也想给大家传个话,没有必要神话大厂,大厂也有 bug,大厂的产品也会服务中断。比如火山云旗下云游戏产品的 OpenApi 接口文档示例 demo 简陋,手动生成签名代码场景单一,覆盖不全等问题,最后就是竟然还返回了一个 null 给我!不过此次对接过程中,在我反馈 OpenApi 接口各种问题时,群里小伙伴都能及时回应以及拉群沟通查看问题解决问题的态度点个赞

标签:String,示例,接口,OpenApi,文档,new,bug,字节
From: https://www.cnblogs.com/waynaqua/p/18196343

相关文章

  • debug技巧之远程调试
    一、前言大家好啊,我是summo,今天给大家分享一下我平时是怎么调试代码的,不是权威也不是教学,就是简单分享一下,如果大家还有更好的调试方式也可以多多交流哦。当我们的应用发布到线上之后,就不能随意启停了,但如果线上出现了BUG怎么办呢?大多数时候我们会借助线上打印的日志进行排查问......
  • Bug搬运工-support of DACL on 9800 WLC platform.
    supportofDACLon9800WLCplatform.CSCvv16183 Symptom:noneConditions:9800runninganycodeandanymodelofAP.Workaround:noneFurtherProblemDescription:dACLwasaddedinrelease17.10 Requestfor9800tosupportDACLCSCvw89561 Symptom:......
  • JavaScript object array sort by string bug All In One
    JavaScriptobjectarraysortbystringbugAllInOnebug//purestringsarray,sortOK✅letarr=["banana","strawberry","apple"];JSON.stringify(arr.sort());//'["apple","banana","strawbe......
  • pycdc不支持的字节码处理
    从官方源编译的pycdc,直接反编译,可以看到存在不支持的字节码BEGIN_FINALLY(理论上遇到类似不支持的字节码,都可以这样处理)参考issue307,处理一下重新编译这时候就可以成功反编译(当然有可能存在一些逻辑问题?不过这里的字节码是涉及异常处理的,对主要逻辑影响不大) ......
  • stm32f103c8t6对flash进行操作,Hal库,擦除1页数据大小,写入128字节大小,读取指定地址128字
    参考这篇:STM32IAP应用开发——自制BootLoader-CSDN博客把工程转到HAL库使用的函数,用HAL自带的HAL_FLASHEx_EraseHAL_FLASH_Program 串口显示结果 验证没问题flash在hal库使用的驱动程序#include"flash.h"externvoidFLASH_PageErase(uint32_tPageAddress);//......
  • 桌面图标间距Bug:Win10/Win11桌面图标占用空间变成长方形怎么办?
    阅读全文:https://itxiaozhang.com/win10-win11-desktop-icon-bug-rectangular-fix/此教程配合视频学习效果最佳,视频教程在文章末尾。问题描述在使用Windows10或Windows11操作系统时,桌面图标的间距突然变得很大,变成了长方形。该问题通常发生在修改屏幕分辨率、连接外部显示......
  • 桌面图标间距Bug:Win10/Win11桌面图标占用空间变成长方形怎么办?
    阅读全文:https://itxiaozhang.com/win10-win11-desktop-icon-bug-rectangular-fix/此教程配合视频学习效果最佳,视频教程在文章末尾。问题描述在使用Windows10或Windows11操作系统时,桌面图标的间距突然变得很大,变成了长方形。该问题通常发生在修改屏幕分辨率、连接外部显示......
  • OpenAI 发布全新生成式模型 GPT-4o;字节收购音频设备公司 Oladance丨 RTE 开发者日报 V
      开发者朋友们大家好: 这里是「RTE开发者日报」,每天和大家一起看新闻、聊八卦。我们的社区编辑团队会整理分享RTE(RealTimeEngagement)领域内「有话题的新闻」、「有态度的观点」、「有意思的数据」、「有思考的文章」、「有看点的会议」,但内容仅代表编辑......
  • 关于编译的一些debug记录
    1.背景:现在想将嵌入式开发板部署成边缘控制器,需要在开发板上部署一个服务。我们已经写了一个c语言的应用,现在需要将其编译成适配64位arm架构的2进制可执行文件2.bug:在编译的时,输入命令:g++-fdiagnostics-color=always-gunifiedAccessServer.cppbaseModels/control/ec/ec......
  • Advanced .Net Debugging 8:线程同步
    一、介绍这是我的《Advanced.NetDebugging》这个系列的第八篇文章。这篇文章的内容是原书的第二部分的【调试实战】的第六章【同步】。我们经常写一些多线程的应用程序,写的多了,有关多线程的问题出现的也就多了,因此,最迫切的任务就是提高解决多线程同步问题的能力。这一节......