亚洲欧洲视频,三男玩一女摸吃奶,久久久久久久片,精品中文一区二区三区,美女在线国产,国产有码视频,亚洲激情五月

威勢(shì)網(wǎng)絡(luò),為您的企業(yè)和團(tuán)隊(duì)注入互聯(lián)網(wǎng)活力!
服務(wù)熱線:138-9741-0341

安全地在前后端之間傳輸數(shù)據(jù) - 「1」技術(shù)預(yù)研

發(fā)布日期:2022/11/10 作者: 瀏覽:793

已經(jīng)不是第一次寫這個(gè)主題了,最近有朋友拿 5 年前的《Web 應(yīng)用中保證密碼傳輸安全》來(lái)問我:“為什么按你說(shuō)的一步步做下來(lái),后端解不出來(lái)呢?”加解密這種事情,差之毫厘謬以千里,我認(rèn)為多半就是什么參數(shù)沒整對(duì),仔細(xì)查查改對(duì)了就行。代碼拿來(lái)一看,傻眼了……沒毛病啊,為啥解不出來(lái)呢?

時(shí)間久遠(yuǎn),原文附帶的源代碼已經(jīng)下不下來(lái)了。翻閱各種參考鏈接的時(shí)候從 CodeProject 上找了個(gè)代碼,把各參數(shù)換過去一試,沒毛病呀!這可奇了怪了,于是去 RSA.js 的文檔(沒有專門的文檔,就是文檔注釋)中查,發(fā)現(xiàn) RSA.js 在 2014 年 1 月加入了 Padding 參數(shù),《Web 應(yīng)用中保證密碼傳輸安全》雖然是 2014 年 2 月寫的,但可能陰差陽(yáng)錯(cuò)用到了老版本。

不就是 Padding 嗎,文檔也懶得看了,前后端都指定 PKCS1Padding 試試。失??!

那暴力一點(diǎn),所有 Padding 都試試!

前端使用 RSA.js 在 RSAAPP 中定義的 4 種 Padding,后端 C# 使用 RSAEncryptionPadding 中定義的 5 種 Padding,組合了 20 種情況,逐一試驗(yàn)……好吧,沒一個(gè)對(duì)的!

世界上這么多樹,何必非要在這一棵上吊死,何況它還沒有發(fā)布到 npm …… 理由找夠了,咱就換!

網(wǎng)上搜了一圈之后,選擇了 JSEncrypt 這個(gè)庫(kù)。

核心知識(shí)

在講 JSEncrypt 之前,咱們回到“安全傳輸”這一主題。這一主題的關(guān)鍵技術(shù)在于加解密,說(shuō)起加解密,那就是三大類算法:HASH(摘要)算法、對(duì)稱加密算法和非對(duì)稱加密算法?;镜陌踩珎鬏斶^程可以用一張圖來(lái) 展示:

不過這只是最基本的安全傳輸理論,實(shí)際上,證書(公鑰)分發(fā)等方面仍然存在安全隱患,所以才會(huì)有CA、才會(huì)有受信根證書……不過這里不作延展,只給個(gè)結(jié)論:在 Web 前后端傳輸這個(gè)問題上,HTTPS 就是最佳實(shí)踐,是首選 Web 傳輸解決方案,只有在不能使用 HTTPS 的情況,才退而求其次,用自己的實(shí)現(xiàn)來(lái)提高一點(diǎn)安全門檻。

JSEncrypt

JSEncrypt 一個(gè)月前剛有新版本,還算活躍。不過在使用方式上跟 RSA.js 不同,它不需要指定 RSA 的參數(shù),而是直接導(dǎo)入一個(gè) PEM 格式的密鑰(證書)。關(guān)于證書格式呢,就不在這里科普了,總之 PEM 是一種文本格式,Base64 編碼。

既然 JSEnrypt 需要導(dǎo)入密鑰,這里主要是需要導(dǎo)入公鑰。我們來(lái)看看 C# 里 RSACryptoServiceProvider 能導(dǎo)出些什么,搜了一下 Export... 方法,導(dǎo)出公約相關(guān)的主要就這兩個(gè):

因?yàn)樵夹枨笫怯?.NET,所以先研究 .NET 跟 JSEncrypt 的配合,后面再補(bǔ)充 NodeJS 和 Java 的。
  • ExportRSAPublicKey(),以 PKCS#1 RSAPublicKey 格式導(dǎo)出當(dāng)前密鑰的公鑰部分。
  • ExportSubjectPublicKeyInfo(),以 X.509 SubjectPublicKeyInfo 格式導(dǎo)出當(dāng)前密鑰的公鑰部分。

還有兩個(gè) Try... 前綴的方法作用相似,可以忽略。這兩個(gè)方法的區(qū)別就在于導(dǎo)出的格式不同,一個(gè)是 PKCS#1 (Public-Key Cryptography Standards),一個(gè)是 SPKI (Subject Public Key Info)。

JSEncrypt 能導(dǎo)入哪種格式呢?文檔里沒明確說(shuō)明,不妨試試。

C# 產(chǎn)生密鑰并導(dǎo)出

C# 中產(chǎn)生 RSA 密鑰對(duì)比較簡(jiǎn)單,使用 RSACryptoServiceProvider 就行,比如產(chǎn)生一對(duì) 1024 位的 RSA 密鑰,并以 XML 格式導(dǎo)出:

// C# Code private RSACryptoServiceProvider GenerateRsaKeys(int keySize = 1024) { var rsa = new RSACryptoServiceProvider(keySize); var xmlPrivateKey = rsa.ToXmlString(true); // 如果需要單獨(dú)的公鑰部分,將傳入 `ToXmlString()` 改為 false 就好 // var xmlPublicKey = rsa.ToXmlString(false); File.WriteAllText("RSA_KEY", xmlPrivateKey); return rsa;
}

為了能在進(jìn)程每次重啟都使用相同的密鑰,上面的示例將產(chǎn)生的 xmlPrivateKey 保存到文件中,重啟進(jìn)程時(shí)可以嘗試從文件加載導(dǎo)入。注意,由于私鑰包含公鑰,所以只需要保存 xmlPrivateKey 就夠了。那么加載的過程:

// C# Code private RSACryptoServiceProvider LoadRsaKeys() { if (!File.Exists("RSA_KEY")) { return null; } var xmlPrivateKey = File.ReadAllText("RSA_KEY"); var rsa = new RSACryptoServiceProvider();
    rsa.FromXmlString(xmlPrivateKey); return rsa;
}

先嘗試導(dǎo)入,不成再新生成的過程就一句話:

// C# Code var rsa = LoadRsaKeys() ?? GenerateRsaKeys();

導(dǎo)出 XML Key 是為了持久化。JSEncrypt 需要的是 PEM 格式的證書,也就是 Base64 編碼的證書。ExportRSAPublicKey 和 ExportSubjectPublicKeyInfo 這兩個(gè)方法的返回類型都是 byte[],所以需要對(duì)它們進(jìn)行 Base64 編碼。這里使用 Viyi.Util 提供的 Base64Encode() 擴(kuò)展方法來(lái)實(shí)現(xiàn):

// C# Code var pkcs1 = rsa.ExportRSAPublicKey().Base64Encode(); var spki = rsa.ExportSubjectPublicKeyInfo().Base64Encode();

嚴(yán)格的說(shuō),PEM 格式還應(yīng)該加上 -----BEGIN PUBLIC KEY----- 和 -----END PUBLIC KEY----- 這樣的標(biāo)頭標(biāo)尾,Base64 編碼也應(yīng)該按每行 64 個(gè)字符進(jìn)行折行處理。不過實(shí)測(cè) JSEncrypt 導(dǎo)入時(shí)不會(huì)要求這么嚴(yán)格,省了不少事。

剩下的就是將 pkcs1 和 spki 傳遞給前端了。Web 應(yīng)用直接通過 API 返回一個(gè) JSON,或者 TEXT 都行,根據(jù)接口規(guī)范來(lái)決定。當(dāng)然也可以通過拷貝/粘貼的方式來(lái)傳遞。這里既然是在做實(shí)驗(yàn),那就用 Console.WriteLine 輸出到控制臺(tái),通過剪貼板來(lái)傳遞好了。

我這里 PKCS#1 導(dǎo)出的是長(zhǎng)度為 188 個(gè)字符的 Base64:

MIGJAoGB...tAgMBAAE=

SPKI 導(dǎo)出的是長(zhǎng)度為 216 個(gè)字符的 Base64:

MIGfMA0GC...QIDAQAB

JSEncrypt 導(dǎo)入公鑰并加密

JSEncrypt 提供了 setPublicKey() 和 setPrivateKey() 來(lái)導(dǎo)入密鑰。不過文檔中提到它們其實(shí)都是 setKey() 的別名,這點(diǎn)需要注意一下。為了避免語(yǔ)義不清,我建議直接使用 setKey()。

You can use also setPrivateKey and setPublicKey, they are both alias to setKey

from: http://travistidwell.com/jsen...

那么導(dǎo)入公鑰并試驗(yàn)加密的過程大概會(huì)是這樣:

// JavaScript Code const pkcs1 = "MIGJAoGB...tAgMBAAE="; // 注意,這里的 KEY 值僅作示意,并不完整 const spki = "MIGfMA0GC...QIDAQAB"; // 注意,這里的 KEY 值僅作示意,并不完整 [pkcs1, spki].forEach((pKey, i) => { const jse = new JSEncrypt();
    jse.setKey(pKey); const eCodes = jse.encrypt("Hello World"); console.log(`[${i} Result]: ${eCodes}`);
});

運(yùn)行后得到輸出(密文也是省略了中間很長(zhǎng)一串的 ):

[0 Result]: false [1 Result]: ZkhFRnigoHt...wXQX4=

看這結(jié)果,沒啥懸念了,JSEncrypt 只認(rèn) SPKI 格式。

不過還得去 C# 中驗(yàn)證這個(gè)密文是可以解出來(lái)的。

C# 驗(yàn)證可以解密 JSEncrypt 生成的密文

上面生成的那一段 ZkhFRnigoHt...wXQX4= 拷貝到 C# 代碼中,用來(lái)驗(yàn)證解密。C# 使用 RSACryptoServiceProvider.Decrypt() 實(shí)例方法來(lái)解密,這個(gè)方法的第 1 個(gè)參數(shù)是密文,類型 byte[],是以二進(jìn)制數(shù)據(jù)的形式提供的。

第二個(gè)參數(shù)可以是 boolean 類型,true 表示使用 OAEP 填充方式,false 表示使用 PKCS#1 v1.5;這個(gè)參數(shù)也可以是 RSAEncryptionPadding 對(duì)象,直接從預(yù)定義的幾個(gè)靜態(tài)對(duì)象中選擇一個(gè)就好。這些在文檔中都說(shuō)得很清楚。因?yàn)橐话愣际鞘褂玫?PKCS 填充方式,所以這次賭一把,直接上:

// C# Code var eCodes = "ZkhFRnigoHt...wXQX4="; // 示例代碼這里省略了中間大部分內(nèi)容 var rsa = LoadRsaKeys(); // rsa 肯定是使用之前生成的密鑰對(duì),要不然沒法解密 byte[] data = rsa.Decrypt(eCodes.Base64Decode(), false);
Console.WriteLine(data.GetString()); // GetString 也是 Viyi.Util 中定義的擴(kuò)展方法,默認(rèn)用 UTF8 編碼

結(jié)果正如預(yù)期:

Hello World

技術(shù)總結(jié)

現(xiàn)在,通過實(shí)驗(yàn),Web 前端使用 JSEncrypt 和 .NET 后端之間已經(jīng)實(shí)現(xiàn)了 RSA 加/解密來(lái)完成安全的數(shù)據(jù)傳輸。其做法總結(jié)如下:

  1. 后端產(chǎn)生 RSA 密鑰對(duì),保存?zhèn)溆?。保存方式可根?jù)實(shí)際情況選擇:內(nèi)存、文件、數(shù)據(jù)庫(kù)、緩存服務(wù)等
  2. 后端以 SPKI 格式導(dǎo)出公鑰(別忘了 Base64 編碼),通過某種業(yè)務(wù)接口形式傳遞給前端,或由前端主動(dòng)請(qǐng)求獲得(比如調(diào)用特定 API)
  3. 前端使用 JSEncrypt,通過 setKey() 導(dǎo)入公鑰,使用 encrypt() 加密字符串。加密前字符串會(huì)按 UTF8 編碼成二進(jìn)制數(shù)據(jù)。
  4. 后端獲得前端加密后的數(shù)據(jù)(Base64 編碼)后,解密成二進(jìn)制數(shù)據(jù),并使用 UTF8 解碼成文本。

特別需要注意的一點(diǎn)是:不管以何種方式(XML、PEM 等)將公鑰傳送給前端的時(shí)候,都切記不要把私鑰給出去了。這尤其容易發(fā)生在使用 .ToXmlString(true) 之后再直接把結(jié)果送給前端。不要問我為什么會(huì)有這么個(gè)提醒,要問就是因?yàn)椤乙娺^!

關(guān)門放 Node

還沒完呢,前面說(shuō)過要補(bǔ)充 NodeJS 后端的情況。NodeJS 關(guān)于加/解密的 SDK 都在 crypto 模塊中,

  • 使用 generateKeyPair() 或 generateKeyPairSync() 來(lái)產(chǎn)生密鑰對(duì)
  • 使用 privateDecrypt() 來(lái)解密數(shù)據(jù)
generateKeyPair() 是異步操作?,F(xiàn)在 Node 中異步函數(shù)很常見,尤其是寫 Web 服務(wù)端的時(shí)候,到處都是異步。不喜歡回調(diào)方式的話,可以使用 util 模塊中的 promisify() 把它轉(zhuǎn)換一下。
// JavaScript Code, in Node environtment import { promisify } from "util"; import crypto from "crypto"; const asyncGenerateKeyPair = promisify(crypto.generateKeyPair);

(async () => { const { publicKey, privateKey } = await asyncGenerateKeyPair( "rsa",
        { modulusLength: 1024, publicKeyEncoding: { type: "spki", format: "pem",
            }, privateKeyEncoding: { type: "pkcs1", format: "pem" }
        }
    ); console.log(publicKey) console.log(privateKey);
})();

generateKeyPair 第 1 個(gè)參數(shù)是算法,很明顯。第 2 個(gè)參數(shù)是選項(xiàng),強(qiáng)度 1024 也很明顯。只有 publicKeyEncoding 和 privateKeyEncoding 需要稍微解釋一下 —— 其實(shí)文檔也說(shuō)得很明白:參考 keyObject.export()。

對(duì)于公鑰,type 可選 "pkcs1" 或者 "spki",之前已經(jīng)試過,JSEncrypt 只認(rèn) "spki",所以沒得選。

對(duì)于私鑰,RSA 只能選 "pkcs1",所以還是沒得選。

不過 NodeJS 的 PEM 輸出要規(guī)范得多,看(同樣省略了中間部分):

-----BEGIN PUBLIC KEY----- MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCYur0zYBtqOqs98l4rh1J2olBb ... ... ... 8I8y4j9dZw05HD3u7QIDAQAB -----END PUBLIC KEY----- -----BEGIN RSA PRIVATE KEY----- MIICXAIBAAKBgQCYur0zYBtqOqs98l4rh1J2olBbYpm5n6aNonWJ6y59smqipfj5 ... ... ... UJKGwVN8328z40R5w0iXqtYNvEhRtYGl0pTBP1FjJKg= -----END RSA PRIVATE KEY-----

不管是否含標(biāo)頭/標(biāo)尾,也不管是不是有折行,JSEncrypt 都認(rèn),所以倒不用太在意這些細(xì)節(jié)??傊?JSEncrypt 拿到公鑰之后還是跟之前一樣,做同樣的事情,邏輯代碼一個(gè)字都不用改。

然后回到 NodeJS 解密:

// JavaScript Code, in Node environtment import crypto from "crypto"; const eCodes = "ZkhFRnigoHt...wXQX4="; // 作為示例,偷個(gè)懶就用之前的那一段了 const buffer = crypto.privateDecrypt(
    { key: privateKey, padding: crypto.constants.RSA_PKCS1_PADDING }, Buffer.from(eCodes, "base64")
); console.log(buffer.toString());

privateDecrypt() 第 1 個(gè)參數(shù)給私鑰,可以是之前導(dǎo)出的私鑰 PEM,也可以是沒導(dǎo)出的 KeyObject 對(duì)象。需要注意的是必須要指定填充方式是 RSA_PKCS1_PADDING,因?yàn)槲臋n說(shuō)默認(rèn)使用 RSA_PKCS1_OAEP_PADDING。

還有一點(diǎn)需要注意的是別忘了 Buffer.from(..., "base64")。

解密的結(jié)果是保存在 Buffer 中的,直接 toString() 轉(zhuǎn)成字符串就好,顯示指定 UTF-8,用 toString("utf-8") 當(dāng)然也是可以的。

等等,還有 Java 呢

Java 也大同小異,不過說(shuō)實(shí)在,代碼量要大不少。為了干這些事情,大概需要導(dǎo)入這么些類:

// Java Code import java.nio.charset.StandardCharsets; import java.security.KeyFactory; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.spec.PKCS8EncodedKeySpec; import java.util.Base64; import java.util.Base64.Decoder; import java.util.Base64.Encoder; import javax.crypto.Cipher;

然后是產(chǎn)生密鑰對(duì)

// Java Code KeyPairGenerator gen = KeyPairGenerator.getInstance("RSA");
gen.initialize(1024); KeyPair pair = gen.generateKeyPair(); Encoder base64Encoder = Base64.getEncoder(); String publicKey = base64Encoder.encodeToString(pair.getPublic().getEncoded()); String privateKey = base64Encoder.encodeToString(pair.getPrivate().getEncoded()); // 這里輸出 PKCS#8,所以解密時(shí)需要用 PKCS8EncodedKeySpec System.out.println(pair.getPrivate().getFormat());

產(chǎn)生的 publicKey 和 privateKey 都是純純的 Base64,沒有其他內(nèi)容(沒有標(biāo)頭/標(biāo)尾等)。

然后是解密過程……

// Java Code String eCode = "k7M0hD....qvdk="; // 再次聲明,這是僅為演示寫的閹割版數(shù)據(jù) Decoder base64Decoder = Base64.getDecoder(); PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(base64Decoder.decode(privateKey)); KeyFactory keyFactory = KeyFactory.getInstance("RSA"); Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
cipher.init(Cipher.DECRYPT_MODE, keyFactory.generatePrivate(keySpec)); byte[] data = cipher.doFinal(base64Decoder.decode(eCode));

System.out.println(new String(data, StandardCharsets.UTF_8));

尾聲

寫完 Java 是真累,所以,以后的后端示例就用 NodeJS 了 —— 不是 Java 的鍋,主要是不想切環(huán)境。

下節(jié)看點(diǎn):「注冊(cè)」的 DEMO,安全傳輸和保存用戶密碼。「?jìng)魉烷T」

文章轉(zhuǎn)自:https://segmentfault.com/a/1190000039827138/


下拉加載更多評(píng)論
最新評(píng)論
暫無(wú)!