我發現了位元組OpenApi介面的bug!

来源:https://www.cnblogs.com/waynaqua/p/18196343
-Advertisement-
Play Games

本文記錄我在對接位元組旗下產品火山雲旗下雲游戲產品 OpenApi 介面文檔時遇到的坑,希望能幫助大家(火山雲旗下雲游戲產品的文檔坑很多,我算是從零到一都踩了一遍,特此記錄,希望大家引以為鑒)。 1. 文檔問題 很經典的開局一張圖,對接全靠問, 這裡給大家強調下,當要跟第三方產品對接時,一定要確認拿到 ...


本文記錄我在對接位元組旗下產品火山雲旗下雲游戲產品 OpenApi 介面文檔時遇到的坑,希望能幫助大家(火山雲旗下雲游戲產品的文檔坑很多,我算是從零到一都踩了一遍,特此記錄,希望大家引以為鑒)。

1. 文檔問題

很經典的開局一張圖,對接全靠問,

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,說乾就乾,直接反饋給火山那邊。

接著火山那邊的人就聯繫說下午兩點開會一起遠程共用我的屏幕看看,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 介面各種問題時,群里小伙伴都能及時回應以及拉群溝通查看問題解決問題的態度點個贊

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 項目場景: 之前正式一直都是使用的手動註冊,而且測試環境還沒有註冊上去,但是最近開發的需要每天在測試上跑跑看,再上生產,所以這次要解決掉。 問題描述 我是測試環境和正式環境都在一臺機子上,兩個環境項目跑不同的docker容器,我要做的是這兩個容器裡面的項目都註冊到我的xxl服務上去。 而且之前情況是 ...
  • 1、先將字體複製到目錄:\vendor\mpdf\mpdf\ttfonts2、再修改文件\vendor\mpdf\mpdf\src\Config\FontVariables.php,對照已經添加的simsun,simkai,simhei,fangzhenweibei,fangzhenheiti等字體 ...
  • nginx 反向代理是什麼? nginx 反向代理是一種網路伺服器架構模式,它通過將客戶端的請求轉發到後端伺服器,來提供負載均衡、高可用性、安全性等功能。 在這種架構中,nginx 作為反向代理伺服器,接收來自客戶端的請求,並將請求轉發到後端伺服器上。 在反向代理架構中,客戶端不直接訪問後端伺服器, ...
  • 請註意以下繼承體系中各class的constructors寫法: 1 class CPoint 2 { 3 public: 4 CPoint(float x=0.0) 5 :_x(x){} 6 7 float x() {return _x;} 8 void x(float xval){_x=xval ...
  • NumPy 分割數組 NumPy 提供了 np.array_split() 函數來分割數組,將一個數組拆分成多個較小的子數組。 基本用法 語法: np.array_split(array, indices_or_sections, axis=None) array: 要分割的 NumPy 數組。 i ...
  • REST(Representational State Transfer),表現形式狀態轉換,它是一種軟體架構風格 當我們想表示一個網路資源的時候,可以使用兩種方式: 傳統風格資源描述形式 http://localhost/user/getById?id=1 查詢id為1的用戶信息 http://l ...
  • title: Django 自定義管理命令:從入門到高級 date: 2024/5/16 18:34:29 updated: 2024/5/16 18:34:29 categories: 後端開發 tags: Django 自定義命令 入門教程 高級技巧 命令創建 命令使用 自定義管理 第 1 章 ...
  • 目錄簡介工作流程核心架構核心模塊介紹DataX調度流程支持的數據實踐下載環境執行流程引用 簡介 DataX是一個數據同步工具,可以將數據從一個地方讀取出來並以極快的速度寫入另外一個地方。常見的如將mysql中的數據同步到另外一個mysql中,或者另外一個mongodb中。 工作流程 read:設置一 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...