🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付

我是如何將手動的日報完全自動化的☺️☺️☺️

書接上回,上回我們聊了處理重複任務的自動化思維。

其中,我舉了用工具自動化公司日報的例子。

今天,我就來詳細說說,我到底是怎麼做的,以及過程中遇到了哪些問題和挑戰。

背景

我們公司使用某第三方系統有一個自訂的數據看板,每天需要向群裡發送日報。之前,這項工作由團隊成員輪流手動完成:從系統的自訂看板複製數據到 Excel,再將表格轉為圖片,發到群裡。

輪到我負責的那一週,我左手邊電腦打開系統,右手邊打開 Excel,一個個數據複製過去,3.4%、-10%……為避免出錯,還要逐一核對。整個過程每天耗時大約 7 到 10 分鐘,繁瑣又枯燥。

我開始思考:這種重複性工作能不能自動化?

於是,我在群裡向大佬們請教,提出了這個問題:

image.png

結果,消息已讀,沒有一個人回覆。

那一刻,我暗下決心:我要自己解決這個問題!

初探

於是乎我打開了該系統,開始研究。

該系統大概長這樣,這是一個自訂看板,後台自訂配置出來的,數據是根據配置的規則算出來的,有十幾個項目,我們是需要從每項取 3 個數據。加起來複製 30-40 次。

image.png

  • 手動複製效率低下。
  • 浪費時間。
  • 容易出錯,黏錯位置了,又得一個個重新對一遍。

所以我第一步是需要把手動複製拿數據的這個過程,利用腳本自動化了。

流程與任務拆解

我們的思路是這樣,先腦子裡過一下原來的流程,然後一步步自動化原來的流程。

1、原來手動的流程

  1. 手動登錄系統
  2. 點擊對應面板,一個個複製數據,黏貼到 Excel 裡。
  3. 複製為圖片
  4. 發送到群裡。

2、腳本任務拆解

  1. js 逆向登錄加密方法,自動化登錄,拿到 token。
  2. 利用爬蟲抓取數據,拿到我需要的。
  3. 利用 canvas 將數據畫成表格,然後轉成圖片。
  4. 圖片傳到 oss,調用釘釘 webhook 接口,定時發送到群裡。

以上我們已經將,手動的流程的任務與自動化需要做的任務一一對應了。

現在我們思路清晰了。

然後我們要做的就是把每個任務逐個攻克即可。

任務分步實現

你不覺得我應該先完成第一個任務——JS 逆向登錄加密方法,實現自動化登錄並獲取 token 嗎?

這確實是全自動流程中最核心的一環:沒有自動登錄獲取憑證,後續的數據抓取和操作根本無從談起。

不過,我初步分析了登錄接口,發現參數加密邏輯較複雜,短時間內難以破解。

於是我選擇暫時跳過,先手動複製登錄憑證,確保後續流程全部打通後再回過頭補全自動化登錄部分。

1、利用爬蟲抓取數據。

首先看板這是一個列表,有很多項目,首先看這個列表怎麼來的,伺服器端渲染還是,調的接口。

然後看能不能完全從頁面拿到,我們再考慮抓取方式。

  1. 如果是伺服器端渲染的或者數據很快出來的。我們可以考慮抓頁面。
  2. 但是今天這個例子,經過我的研究,我需要的數據,都是異步調接口的,我看還有隊列排隊邏輯。說明頁面完整展現的時間不穩定,長則幾十秒都有可能,所以我感覺抓頁面是不穩定的。

所以我選擇抓接口

1.1 內容搜尋大法

image.png

眾多接口,我們怎麼找到我要的數據在哪,於是我們利用調試工具,搜尋響應內容關鍵字。

例如搜頁面中顯示的這個標題

image.png

通過內容再 network 搜索內容 找到了列表接口

image.png

點開看,確實,裡邊就是這個列表的數據。

但是沒有具體是環比、同比,我要的數字。

再次尋找每一項具體數據的獲取接口。

再次通過搜尋大法找了好久.....

找到了通過每項 id 和過濾條件去獲取具體數據的接口。

1.2 接口找齊,開始編碼

研究下來。整體邏輯是,先獲取面板列表,然後循環列表的每一項,拿著有關聯的參數去調詳情。

面板的數據列表獲取
/**
 * 獲取重點功能監控面板列表及詳情數據
 * @returns {Promise<*[]>}
 */
async function queryReportList(dashboard) {
    const { id: dashboard_id, common_event_filter } = dashboard

    const data = await fetch(
        `https://xxx/api/v2/sa/dashboards/${dashboard_id}?is_visit_record=true`,
        {
            credentials: "include",
            headers: {
                "User-Agent":
                    "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:136.0) Gecko/20100101 Firefox/136.0",
                Cookie: Cookie
            },
            referrer:
                `https://xxx/dashboard/?dash_type=lego&id=${dashboard_id}&project=fotorglobalproduct&product=sensors_analysis`,
            method: "GET",
            mode: "cors"
        }
    )
        .then(res => res.json())

    const result = [];
    // 獲取控面板的前 12 個監控項的監控數據。
    for (const item of data.items.slice(0, 13)) {
        if (item.bookmark) {
            // 這裡解出來, 調下個接口要用到。
            const data = JSON.parse(item.bookmark.data);
            const res = await queryReportByTool({
                bookmarkid: item.bookmark.id,
                measures: data.measures,
                dashboard_id: dashboard_id,
                common_event_filter: common_event_filter
            });
            result.push({
                ...res,
                name: item.bookmark.name
            });

            console.log(
                {
                    name: item.bookmark.name,
                    base_number: res.base_number /= 100,
                    day: res.month_on_month /= 100,
                    week: res.year_on_year /= 100
                }
            )
        }
    }

    return result
}
獲取每一項具體數據
/**
 * 報告列表的報告 id 去獲取具體數據
 * @param params
 * @returns {Promise<T|*|undefined>}
 */
async function queryReportByTool(params) {
    const requestId = Date.now() + ":803371";
    const body = {
        measures: params.measures,
        unit: "day",
        by_fields: [],
        sampling_factor: null,
        from_date: dayjs()
            .subtract(14, "day")
            .format("YYYY-MM-DD"),
        // from_date:  "2025-02-28",
        to_date: getYesterDay(),
        // to_date: "2025-03-13",
        detail_and_rollup: true,
        enable_detail_follow_rollup_by_values_rank: true,
        ...
    };
    try {
        const data = await fetch(
            `https://xxxx/api/events/compare/report/?bookmarkId=${params.bookmarkid}&async=true&timeout=10&request_id=${requestId}`,
            {
                credentials: "include",
                headers: {
                    "User-Agent":
                        "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:136.0) Gecko/20100101 Firefox/136.0",
                    ...
                    Cookie
                },
                referrer:
                    "https://xxxx/dashboard/?dash_type=lego&id=692&project=fotorglobalproduct&product=sensors_analysis",
                body: JSON.stringify(body),
                method: "POST",
                mode: "cors",
                timeout: 10000
            }
        ).then(res => res.json());
        if (!data || data.isDone === false) {
            return await queryReportByTool(params);
        } else {
            return data;
        }
    } catch (e) {
        return await queryReportByTool(params);
    }
}

1.3 數據拿到

執行一下,數據拿到了,找到了我要的幾個字段。

PS D:\project2\report> node .\index.js
{
  name: 'xxx生成失敗率',
  base_number: 0.0103,
  day: -0.3602,
  week: -0.16260000000000002
}
...
{
  name: 'xxxx生成失敗率',
  base_number: 0.017,
  day: 0,
  week: 0.0241
}
2025-03-18.xlsx檔案已保存!
default: 27.917s

2、生成圖片

node-cavas 生成圖片。

3、圖片傳到 oss,調用釘釘 webhook 接口,定時發送到群裡。

傳圖

const filePath = `/custom/999/${dashboard.worksheetName}-${dayjs().format('YYYYMMDD')}.jpeg`
const uploadRes = await tencentCos.upload(imageBuffer, filePath, true)

發釘釘群

async function sendDingTalkMessage(text) {
    // const today = dayjs()
    //     .format("YYYY-MM-DD")
    const token = '1a6e1111111' // 大群機器人
    const result = await fetch(`https://oapi.dingtalk.com/robot/send?access_token=${token}`, {
        method: 'post',
        headers: {
            'Content-Type': 'application/json',
        },
        body: JSON.stringify({
            "msgtype": "markdown",
            "markdown": {
                "title": "監控日報",
                // "text": `#### ${title}-${today} \n  > ![screenshot](${imageUrl}) \n`
                "text": text
            },
            "at": {
                "isAtAll": true
            }
        })
    }).then(res => res.json())
    console.log(result)
    if (result.errcode === 0) {
        console.log('發送成功')
        return true
    }
}

4、js 逆向登錄加密方法,自動化登錄。

就剩下自動登錄了。

4.1 為什麼要自動化登錄?

因為這個系統登錄憑證在一定時間內會過期,且不是明文登錄的,登錄接口參數加密了的。

image.png

看到這你就得去研究他的加密規則了。

或者止步於此,手動複製登錄憑證,本地執行腳本也是可以。

我如果要在伺服器上自動化整個流程,必須得讓他自動登錄拿到登錄憑證。

4.2 逆向步驟

4.2.1 找到登錄接口

先點頁面的登錄,找到登錄接口,在請求調用棧中隨便找個位置先打個斷點,然後刷新頁面,再次點擊登錄,嘿,您猜怎麼著,斷住了!!!

image.png

4.2.2 順著調用棧找邏輯

順著調栈給上找邏輯。所有在前端加密的一定是可以模擬的。

找到了調登錄的方法。
看了下這就是調 store 裡的方法 passport/login。

image.png

從這再往下就比較不容易了,因為你會發現,就有點亂了。進到的都是混淆的一些 abcdefg 名字的方法。

image.png

但是咱們明確目標就是要找到調用的方法 passport/login 的位置。

我嘗試了如下:

  • 搜索 passport/login 關鍵字
  • 搜索接口路徑

找了一輩子, 終於找到了調接口的地方。

看到這個 Me 方法,傳遞了一個 isEncrypted 我猜測就是 是否要加密參數的意思吧。

image.png

別搁 Me 方法外面蹭了行不行???趕緊進去看看。

你就給我看這個?
這裡面又調了另一個。

image.png

咱們接著進到 xt.request。

您猜怎麼著,還沒到,這裡又進行了一頓操作之後,調了一個名為 P 的方法。

image.png

好好好,繼續繼續。

4.2.3 找到了加密的位置

到了 P 方法,終於是沒給我玩套娃了啊。

在這一步終於是看到了關鍵字 isEncrypted

看了代碼確實是判斷 isEncrypted 加密的。

image.png

看了後發現這是一個RSA+AES結合的加密方式

RAS加密密鑰, AES加密登錄數據。

加密流程總結:

  1. RSA保護AES密鑰的安全傳輸
  2. AES保護實際登錄數據的機密性
  3. 雙重加密確保登錄信息在傳輸過程中的安全性
為何不用單一的加密方式?

那麼你有沒有這樣的疑問呢?為什麼不單獨用 rsa 直接加密數據呢?豈不簡單。

當然不行,是有原因的!

RSA長度限制
RSA加密算法對明文長度有嚴格限制,具體取決於密鑰長度和填充方式。以下是不同密鑰長度下的最大明文長度(以字節為單位):

  • 1024 位密鑰:最大明文長度約為 117 字節
  • 2048 位密鑰:最大明文長度約為 245 字節
  • 4096 位密鑰:最大明文長度約為 512 字節

所以 RSA 加密超出長度的會報錯的。

所以先生成短密鑰,再使用 RSA 加密 AES 對稱算法的密鑰,再用對稱密鑰加密實際數據。這是實際應用中的常見做法,兼顧安全性和效率。

4.2.4 模擬他的加密過程
大致流程

所以你需要怎麼做?

  1. 把加密邏輯 copy 啊。
  2. 補環境。
  3. 不斷嘗試直到通過後端校驗。
理解後端如何解密和校驗

前面我們說到。

RAS加密密鑰, AES加密登錄數據。

那麼後端的校驗流程就是:

  1. 私鑰解出密鑰
  2. 密鑰配和 iv、salt 等再解出被 AES 加密的賬號密碼信息。

知道了這些,那麼我們需要做的就是正確加密和傳遞相關信息,如果校驗失敗,我們就要來回對比差異,找到問題,不斷嘗試。

遇到的問題
  • 加密的包的版本跟目標網站用的不一樣導致校驗失敗,後經過漫長的查找找到了一樣的版本。
  • 還要注意 header 裡帶的字段,都要模擬他加密後的帶過去。例如這幾個。

image.png

真的是一個不斷嘗試,不斷解決問題的過程。

最終我抽出來的方法

var b = require("crypto-js");
var jsencrypt = require("nodejs-jsencrypt/bin/jsencrypt").default;

/**
 * js逆向回來的方法,模擬xx登錄對參數加密
 * @param body xx登錄參數
 * @param public 公鑰
 * @returns {{headers: {"aes-salt": string, "aes-iv": string, "aes-passphrase": *, "X-Request-Timestamp": string, "X-Request-Id": string, "X-Request-Sign": *}, body: string}}
 */
function encryptLogin(body, public) {
    const W = new jsencrypt();
    W.setPublicKey(public)

    q = b.enc.Utf8.parse(Math.floor(Math.random() * 1e6) + Date.now()).toString();
    // q = "31373432353237383135363835";
    var re = W.encrypt(q)
        , ie = b.lib.WordArray.random(128 / 8)
        , fe = b.lib.WordArray.random(128 / 8)
        , ue = b.PBKDF2(q, ie, {
            keySize: 128 / 32,
            iterations: 100
        })
        , ye = b.AES.encrypt(JSON.stringify(body), ue, {
            iv: fe,
            mode: b.mode.CBC,
            padding: b.pad.Pkcs7
        });

    const j = "/api/v2/auth/login?is_global=true"
    const Ee = parseInt(Date.now() / 1000).toString()
    const he = Ee
    const Fe = ye.toString()

    var bt = "".concat(Ee, "_").concat(he, "_").concat(j, "_").concat(Fe, "_14skjh");

    const res = {
        headers: {
            "aes-salt": ie.toString(),
            "aes-iv": fe.toString(),
            "aes-passphrase": re,
            "X-Request-Timestamp": Ee,
            "X-Request-Sign": b.MD5(bt).toString(),
            "X-Request-Id": he,
        },
        body: ye.toString()
    }

    return res
}

任務分步都實現了(自動化了)。

串聯起來這四步,整體就實現了。

隨後部署到伺服器,配置定時任務每天執行。

效果展示

image.png

總結

  • 先通後補:登錄逆向卡殼,先手動 Cookie 跑通全鏈,再回填自動化。
  • 逆向不怕亂:混淆代碼裡斷點 + 全局搜索(接口路徑/關鍵字),總能定位加密點。
  • 加密常 RSA+AES:RSA 只加密短密鑰,AES 加密長數據,補環境 + 對齊 Header 字段是關鍵。
  • 貴在堅持:第一天研究無果別灰心,第二天重新上手,靈感與進展常不期而至。

雖然文章寫得像一帆風順,但實則磕磕絆絆——在層層混淆的代碼裡翻找,第一天方法不對,左衝右突腦殼嗡嗡作響。幸好第二天沒放棄,沉下心繼續深挖,一步步試錯、迭代,終於攻克所有難題。

如果有小夥伴想跟我探討細節,歡迎聯繫!

喜歡的話,點點關注。


原文出處:https://juejin.cn/post/7566913899175051299


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝21   💬4   ❤️6
658
🥈
我愛JS
📝1   💬5   ❤️2
48
🥉
酷豪
1
#5
1
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次
🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付