首頁 > 軟體

JS前端介面請求引數混淆方案分享

2022-07-27 18:01:31

寫在前面

在一些介面請求的場景中,我們希望攜帶的資料不希望是以明文的方式提交的,也就是需要對引數做一些混淆或者加密處理,後端拿到資料後再進行解密,得到真實資料。

其目的是為了保護資料的安全,以及提高被識破成明文的門檻。在例如使用者登入的介面請求中,如果賬號和密碼是以明文傳輸的,會容易導致一些安全性問題,同時,我們也不希望誰都可以偽造引數對介面發起請求,因此前端引數混淆非常有必要,這裡分享一個自用的方案,其目的在於:

  • 防止資訊洩露
  • 防止引數被隨意篡改
  • 提高應用安全性和穩定性

對於加密或者混淆的處理方式的選擇,例如base64MD5等安全性不高或者不可逆的演演算法,不適用於我們這個場景,而是可以利用AesRsa兩者結合的方式,實現資料的加解密。

什麼介面的引數需要做處理

從安全形度出發,這裡一致認為,只要是對資料庫有新增、修改、刪除操作的,一律需要做混淆/加密,這一類介面,多為postputdelete等請求。

而對於get請求,如果是規範化的介面,一般只是獲取資料,不會對資料庫有直接的操作,故不需要做引數處理。

當然還有post請求和一些檔案上傳等,不希望做引數處理的介面,需要特殊處理,以及在開發環境中,為了方便偵錯,也不需要做處理。

引數處理

因為Rsa在處理資料量較大時優勢不明顯,適合處理資料量較小的場景,所以對引數資料的處理採用了Aes加密,而參與加密的金鑰key則採用Rsa非對稱加密提高破解難度。

涉及到的相關依賴及其版本號:

"crypto-js": "^4.1.1",
"jsencrypt": "^3.2.1"

這裡使用的方案是,先將原始資料處理為query形式的字串,然後將其使用隨機字串作為金鑰,參與Aes加密,並擷取特定的字串,作為原始金鑰,再進行一次Aes加密,最後將原始金鑰使用與後端約定好的公鑰進行Rsa加密處理,具體流程和演演算法如下:

  • 對引數排序、提取query字串處理
  • 將提取的字串利用隨機字串加密並擷取,得到金鑰
  • 利用金鑰對原始引數進行Aes加密
  • 將金鑰進行Rsa非對稱加密
  • 輸出最終的datakey
/**
* 加密請求資料
* @param {Object} rawData 原始資料
* @returns {data, key}
*/
export function encryptRequestData(rawData) {
 // 字典排序並賦值
 var result = {}, str = [], arr = Object.keys(rawData).sort();
 arr.map(m => { result[m] = rawData[m] });
 // 處理成 query 形式字串
 for (var p in result)
     result.hasOwnProperty(p) && str.push(encodeURIComponent(p) + "=" + encodeURIComponent(result[p]));
     result = str.join("&");
 // 參與 Aes 加密的金鑰,將處理後的字串用 16 位隨機碼對稱加密,再從第 3 位開始獲取 16 位原始金鑰
 const rawKey = aesEncrypt(result, randomString(16)).substr(3, 16);
 // 輸出最後的加密引數
 const data = aesEncrypt(JSON.stringify(rawData), rawKey);
 const key = rsaEncrypt(rawKey);
 return { data, key }
}

Aes加密

/**
 * Aes 加密
 * @param {String} data
 * @param {String} customer_key
 * @returns encrypted
 */
export function aesEncrypt(data, customer_key = "") {
    var key = CryptoJS.enc.Utf8.parse(customer_key);
    var messageHex = CryptoJS.enc.Utf8.parse(data);
    var encrypted = CryptoJS.AES.encrypt(messageHex, key, {
        "mode": CryptoJS.mode.ECB,
        "padding": CryptoJS.pad.Pkcs7
    });
    return encrypted.toString();
}

Rsa加密

/**
 * Rsa 加密
 */
export function rsaEncrypt(rawData) {
    let data;
    try {
        var rsa = new JsEncrypt();
        rsa.setPublicKey(publicPem);
        data = rsa.encrypt(rawData)
    } catch (error) {
        return null
    }
    return data;
}

簽名驗證

如果需要進一步加強防篡改,可以在處理引數的時候,通過一定演演算法得出一個簽名sign值,一併提交到後端,後端解密後,也通過同樣的演演算法將解密後的資料生成一個sign值,與提交的作對比,判斷是否為合法的請求來源。 這裡不做詳細介紹。

處理時機

我們需要在請求攔截的時候對引數做混淆處理,這裡只針對post請求以及非本地環境,另外為了方便偵錯,除了開發環境外 在範例上還增加了uncrypt來標識哪些介面可以不參與處理。

// 請求攔截
instance.interceptors.request.use(
    config => {
        config.headers.contentType = "application/x-www-form-urlencoded"
        const token = local.get('token')
        token && (config.headers.Authorization = token)
        const { method, uncrypt = false, data = {} } = config;
        (method === 'post' && !uncrypt && cfg.NODE_ENV === 'development') && (config.data = encryptRequestData(data));
        return config
    },
    error => Promise.error(error)
)

後端實現

前端引數處理後以datakey組成的物件提交至後端,服務層接受後進行解密,這裡以 Egg.js 的實現為例子。 涉及依賴及其版本號:

"crypto-js": "^4.1.1",
"node-rsa": "^1.1.1"

Rsa解密

// RSA 解密
rsaDecrypt(data) {
    let dataObj;
    return new Promise(function (resolve, reject) {
        // 私鑰存放在app/extend/pem/private.pem
        fs.readFile('app/extend/pem/private.pem', function (err, pem) {
            const key = new NodeRSA(pem, 'pkcs8-private');
            key.setOptions({ encryptionScheme: 'pkcs1' });
            try {
                dataObj = key.decrypt(data, 'utf8');
            } catch (e) {
                const second = new NodeRSA(pem, 'pkcs8-private');
                try {
                    dataObj = second.decrypt(data, 'utf8');
                } catch (error) {
                    reject("Rsa 解密失敗");
                }
            }
            resolve(dataObj);
        });
    });
}

Aes解密

// Aes 解密
aesDecrypt(data, customer_key = "") {
    var key = CryptoJS.enc.Utf8.parse(customer_key || this.config.secret.aes.key);
    var decrypt = CryptoJS.AES.decrypt(data, key, {
        "mode": CryptoJS.mode.ECB,
        "padding": CryptoJS.pad.Pkcs7
    });
    return CryptoJS.enc.Utf8.stringify(decrypt);
}

處理中介軟體

新建解密處理的中介軟體app/middleware/security.js

module.exports = () => {
    return async function (ctx, next) {
        const { helper, request } = ctx;
        const { ajaxMsg } = helper;
        const { key, data } = request.body;
        if (!key || !data) return ajaxMsg(ctx, "-1", "請求引數錯誤", null, 400);
        let rawKey;
        try {
                rawKey = await helper.rsaDecrypt(key);
        } catch (error) {
                return ajaxMsg(ctx, "-1", "金鑰解析失敗", null, 400)
        }
        if (!rawKey) return ajaxMsg(ctx, "-1", "金鑰解析失敗", null, 400);
        const decryptData = JSON.parse(helper.aesDecrypt(data, rawKey));
        if (!decryptData) return ajaxMsg(ctx, "-1", "安全驗證未通過", null, 400);
        request.body = decryptData;
        await next();
    };
};

路由中使用

const { router, controller, middleware } = app;
const security = middleware.security(); // 介面引數加密
router.post('/common/user/login', security, controller.common.user.login); // 登入

以上就是JS前端介面請求引數混淆方案分享的詳細內容,更多關於JS前端介面請求引數混淆的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com