我发现了字节OpenApi接口的bug!
本文记录我在对接字节旗下产品火山云旗下云游戏产品 OpenApi 接口文档时遇到的坑,希望能帮助大家(火山云旗下云游戏产品的文档坑很多,我算是从零到一都踩了一遍,特此记录,希望大家引以为鉴)。
1. 文档问题
很经典的开局一张图,对接全靠问,
这里给大家强调下,当要跟第三方产品对接时,一定要确认拿到的文档是不是最新版本。
比如我在这次对接中,第一次拿到的文档是产品给的,在业务中需要用到一个用户主动退出游戏的接口,于是我在第一份文档里面找到一个用户退出游戏的接口 RomoveUser。
但是当我在控制台调用此接口报错后,去群里一问才发现,对方建议我使用官网公布的最新接口文档。
进入官网发现 RemoveUser 这个接口已经是历史接口了,官方建议换到 BanRoomUser 接口。
OK,这里算是踩到了第一个坑,文档版本不是最新。
ps:还要说一下,火山云旗下云游戏的这个 OpenApi 接口文档需要在群里联系他们开白才能看到,说实话给我的感觉很奇怪,怀疑产品是否有赶鸭子上架问题,暂且怀疑他们的目的是防止不明攻击吧。
2. OpenApi 示例 demo
第三方接口的接入一般都需要做鉴权。火山云旗下云游戏产品的 OpenApi 接口接入当然也不例外。于是我开始了第二个踩坑之旅,那就是他们给出的 OpenApi 示例 demo 的使用过于简单。
火山云旗下云游戏产品的 OpenApi 示例 demo 写的很简单,只提供了一个 GET 请求示例。
OpenApi 示例 demo 地址:https://github.com/volcengine/veGame
但是在我司的业务场景还是上个问题,需要一个用户主动退出游戏的接口,在火山云官网的 OpenApi 文档中我也找到了这个接口,就是上文提到的 BanRoomUser 接口。
但是在官方文档中 BanRoomUser 接口是一个 POST JSON 格式的请求。官方给出的 OpenApi 示例 demo 中并没有关于 POST JSON 请求的示例代码,所以只能靠我一个人查看他们提供的 SDK 依赖源码硬猜来写...,这就很让人头痛了。
好在我翻阅他们 SDK 源码中找到一个靠谱的 json(...) 请求方法,来完成这个 POST JSON 请求。
OK,说干就干,直接写好示例代码,开始发送 POST JSON 请求,
what f**k?什么鬼,返回了我一个 null,此时我的内心中充满了一个大大的问号。
我开始怀疑我的代码是不是写错了。但是当我经历过数次源码 debug 以及调用其他 OpenApi 接口测试并得到正确返回后,我坚定的认为我没错,这就是火山云 OpenApi 的 bug!
OK,说干就干,直接反馈给火山那边。
接着火山那边的人就联系说下午两点开会一起远程共享我的屏幕看看,OK 欣然接收,让他们见证下他们写的 bug!
...
时间来到下午两点,当我共享屏幕给字节工程师演示这个 bug 时,我的控制台打印如下,
woca,竟然不是 null!好在我脑袋灵活,思路清晰,瞬间想到我改了一个参数 GameId,之前返回 null 时,我传的 GameId 是一个假数据,现在我传的是一个真数据。造成了返回不一致。
OK,找到了返回正常的原因,当我把 GameId 改成假数据时,如我所愿,返回了一个 null。
自此,我也就在字节工程师的围观下,复现了他们的 OpenApi 接口的线上 bug。大功告成。
3. 鉴权失败
字节提供的 OpenApi 示例 demo 现在算是跑通了,但是由于我司项目一些依赖限制问题,我们不能直接引入火山云旗下云游戏产品的 SDK 依赖。所以我还得手动编写生成签名的代码。于是我开始了第三个踩坑之旅,那就是 GET 请求验签成功 POST 请求验签失败的问题。
这里先说一下,火山云提供了手动生成签名的示例代码
Java 生成签名的代码:https://github.com/volcengine/volc-openapi-demos/blob/main/signature/java/Sign.java
这里我也是直接把签名代码拿来即用就行,一开始接入生成签名代码非常顺利,GET 请求的 OpenApi 接口都是可以顺利调通的,但是当我调用 BanRoomUser 接口时(没错,又是这个接口,踩的三个坑都与这个接口有关),直接提示验签失败!
OK,开始排查为什么签名失败。
查看源码发现,POST JSON 请求时的 contentType 还是 application/x-www-form-urlencoded
,直觉告诉我这里不对,所以改成 application/json
试试,看看控制台返回,
很好,还是验签失败!!!
我尽力了兄弟们,这个坑踩的我是无话可说。直接联系直接字节开发人员看下我的请求内容是哪里有问题。
在与字节开发人员一起观摩我写的代码以及生成的签名之后,大家都没找到问题所在。那没办法了,只能上服务器看接口请求日志了。
大家可以看出问题在哪里吗?没错我刚刚不是把 contentType 改成了 application/json
吗,为什么日志显示的 contentType 是 application/json; charset=utf-8
!。
OK,到这里问题也找到了,原因是我这个项目用的 http 请求工具是 okhttp3。他自动给我拼接上去的!
那么怎么解决嘞,替换 http3 工具的话,改造成本比较大,所以我就顺势把代码的 contentType 也改成
application/json; charset=utf-8
。
在测试一遍,看看控制台打印。
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 接口各种问题时,群里小伙伴都能及时回应以及拉群沟通查看问题解决问题的态度点个赞。
关注公众号【waynblog】每周分享技术干货、开源项目、实战经验、国外优质文章翻译等,您的关注将是我的更新动力!
我发现了字节OpenApi接口的bug!的更多相关文章
- Java-idea-FindBugs字节码级别潜在bug查看
一.概述 静态分析工具承诺无需开发人员费劲就能找出代码中已有的缺陷. FindBugs 不注重样式或者格式,它试图只寻找真正的缺陷或者潜在的性能问题. FindBugs 是一个静态分析工具,它检查类或 ...
- 本页面用来演示如何通过JS SDK,创建完整的QQ登录流程,并调用openapi接口
QQ登录将用户信息存储在cookie中,命名为__qc__k ,请不要占用 __qc__k : 1) :: 在页面顶部引入JS SDK库: 将“js?”后面的appid参数(示例代码中的:100229 ...
- OpenAPI 接口幂等实现
OpenAPI 接口幂等实现 1.幂等性是啥? 进行一次接口调用与进行多次相同的接口调用都能得到与预期相符的结果. 通俗的讲,创建资源或更新资源的操作在多次调用后只生效一次. 2.什么情况会需要保证幂 ...
- 0422 发现( 数学口袋精灵)bug
团队博客地址: 甘佳萍:http://www.cnblogs.com/gjpg/ 李鹏飞:http://www.cnblogs.com/l549023320/ 赵创佳:http://www.cnblo ...
- 发现C++Builder 2010一组类BUG
今天C++Builder 2010写小码,我们用一个集合类.您可以设置操作结果是不正确的,排除其他原因引起的,最后,它应该被设置以确定问题类的源,以下是一个集合类测试代码: enum TTes ...
- 发现javax.xml.parsers.SAXParser有bug
javax.xml.parsers.SAXParser有bug, 我发现的地方在characters(char[] ch, int start, int length) length偶尔会变小,导致截 ...
- 发现一个c++ vector sort的bug
在开发中遇到一个非常诡异的问题:我用vector存储了一组数据,然后调用sort方法,利用自定义的排序函数进行排序,但是一直都会段错误,在排序函数中打印参加排序的值,发现有空值,而且每次都跟同一个数据 ...
- 阿里云openapi接口使用,PHP,视频直播
1.下载sdk放入项目文件夹中 核心就是aliyun-php-sdk-core,它的配置文件会自动加载相应的类 2.引入文件 include_once LIB_PATH . 'ORG/aliyun-o ...
- 发现了一个entity framework的BUG
小弟学浅才疏可能是小题大做,但遇上了并且让我麻烦了一阵,就值得记下来 BUG的过程就是我在建立实体模型的时候 命名了一个叫system的实体模型 导致了所有生成类中 引用using system失败
- 关于登陆界面,页面没有刷新完毕,点击登陆跳转到一个接口的bug
现象 输入完密码点击登陆就跳转到了如下的页面 分析原因: 第一:查看html页面 页面中的html 登陆用的是form表单 表单中还写了属性 action 即允许跳到某一个接口,这里是没 ...
随机推荐
- #Tarjan#洛谷 5676 [GZOI2017]小z玩游戏
题目 分析 可能玩两次也就是形成环即是Tarjan缩点后在同一个强连通分量 如果按照游戏连边数量将达到\(O(n^2)\),当中其实有很多边可以共用, 考虑\(i\)连向\(i\)的倍数,以及有趣程度 ...
- 准备Python环境学习OpenCV的使用
安装venv模块,执行如下命令: sudo apt-get install python3-venv 创建venv环境,命名为images,执行如下命令: python3 -m venv images ...
- 网络协议之:haproxy的Proxy Protocol代理协议
目录 简介 Proxy Protocol的实现细节 版本1 版本2 Proxy Protocol的使用情况 总结 简介 代理大家应该都很熟悉了,比较出名的像是nginx,apache HTTPD,st ...
- 使用site-maven-plugin在github上搭建公有仓库
目录 简介 前期准备 在maven中配置GitHub权限 配置deploy-plugin 配置site-maven-plugin 怎么使用这个共享的项目 总结 简介 Maven是我们在开发java程序 ...
- Linux之Docker搭建KMS服务器
前言 在大规模软件部署和管理的过程中,激活授权管理是一项至关重要的任务.微软的KMS(Key Management Service)是一个广泛使用的技术,它使得大型组织能够在本地网络内集中管理Wind ...
- 前端之多线程 ---webworker
一.啥是workerJavaScript为单线程,worker则为JavaScript创建多线程环境.使用场景如:计算文件hash,计算大于1G的文件hash过程是很慢的,但由于要将hash传给后端, ...
- C#的基于.net framework的Dll模块编程(一) - 编程手把手系列文章
从此博文开始分几篇介绍C#的开发.这次讲讲C#的.net framework的Dll文件类库模块的编程方法. 对于Windows来说,要运行应用程序要基于Dll类库和Exe执行文件.对于笔者来说,模块 ...
- 技术解读:英特尔 x86 平台上,AI 能力是如何进行演进的?(附PPT)
简介:AI 生态系统是怎样的?其中又有哪些关键技术? AI 计算力的指数增长意味着,为了解决越来越复杂的用例,即使是 1000 倍的计算性能增长也很容易被消耗.因此,需要通过软件生态系统的助力,才能 ...
- 实时 OLAP, 从 0 到 1
简介: BTC.com 团队在实时 OLAP 方面的技术演进过程及生产优化实践. 作者|高正炎 本文主要介绍 BTC.com 团队在实时 OLAP 方面的技术演进过程及生产优化实践,内容如下: 业务背 ...
- IoT Studio可视化搭建平台编辑历史功能的思考与探索
简介: 在前端可视化搭建领域中"重做"和"撤销"这两个功能已经是标配中的标配,毕竟只要有用户行为的地方就可能会有出错,这两个功能无疑就是为用户提供了" ...