https://www.cnblogs.com/smh188/p/11533668.html(我是如何一步步編碼完成萬倉網ERP系統的(一)系統架構)html
https://www.cnblogs.com/smh188/p/11534451.html(我是如何一步步編碼完成萬倉網ERP系統的(二)前端框架)前端
https://www.cnblogs.com/smh188/p/11535449.html(我是如何一步步編碼完成萬倉網ERP系統的(三)登陸)git
https://www.cnblogs.com/smh188/p/11541033.html(我是如何一步步編碼完成萬倉網ERP系統的(四)登陸的具體實現)github
https://www.cnblogs.com/smh188/p/11542310.html(我是如何一步步編碼完成萬倉網ERP系統的(五)產品庫設計 1.產品類別)redis
https://www.cnblogs.com/smh188/p/11546917.html(我是如何一步步編碼完成萬倉網ERP系統的(六)產品庫設計 2.百度Ueditor編輯器)json
https://www.cnblogs.com/smh188/p/11572668.html(我是如何一步步編碼完成萬倉網ERP系統的(七)產品庫設計 3.品牌圖片跨域上傳)後端
https://www.cnblogs.com/smh188/p/11576543.html(我是如何一步步編碼完成萬倉網ERP系統的(八)產品庫設計 4.品牌類別)跨域
https://www.cnblogs.com/smh188/p/11578185.html(我是如何一步步編碼完成萬倉網ERP系統的(九)產品庫設計 5.產品屬性項) 緩存
https://www.cnblogs.com/smh188/p/11589264.html(我是如何一步步編碼完成萬倉網ERP系統的(十)產品庫設計 6.屬性項和類別關聯) 安全
https://www.cnblogs.com/smh188/p/11596459.html(我是如何一步步編碼完成萬倉網ERP系統的(十一)產品庫設計 7.發佈商品)
https://www.cnblogs.com/smh188/p/11610960.html(我是如何一步步編碼完成萬倉網ERP系統的(十二)庫存 1.概述)
https://www.cnblogs.com/smh188/p/11669871.html(我是如何一步步編碼完成萬倉網ERP系統的(十三)庫存 2.加權平均價)
https://www.cnblogs.com/smh188/p/11763319.html(我是如何一步步編碼完成萬倉網ERP系統的(十四)庫存 3.庫存日誌)
萬倉網ERP系統不開源,準備作一個系列,講一講主要的技術點,這些技術點會有源代碼。若是想看全部的源代碼,能夠打道回府了,不必再閱讀下去了,浪費您寶貴的時間。
首先用戶進入到一個後臺系統,確定是先要登陸,這篇我們就說說登陸(固然萬倉網ERP系統開發的第一個頁面並非登陸)。
登陸頁面主要的是注意保護用戶的密碼不能被竊取,不能是明文,不能被窮舉撞庫(簡單密碼),那怎樣才能實現這3個小目標呢?
1.不能被竊取,可使用證書(let's encrypt的免費證書)
2.最好在前端加密後在經過網絡進行傳輸。
3.最好有大小字母、數字和特殊符號組成8位及以上密碼,防止暴力破解。
如今我們主要說第2中狀況,如何在前端使用js加密,後端.Net進行解密?密碼學和TLS的知識不在本文的介紹範圍以內,直接上硬核內容吧,前端使用基於X25519密鑰交換(RSA密鑰交換已通過時,最新的Tls1.3只有ecc橢圓曲線密鑰交換這一種,x25519就屬於ecc橢圓曲線的一種)的aes-128-gcm(加解密速度和安全與一身的加密方式,比上一版本aes-cbc加密安全,cpu硬件內置aes-gcm加密模塊)加密,後端.net進行解密,驗證用戶名和密碼是否正確。
google的證書使用的就是基於X25519的AES_128_GCM證書,等於我們如今手工用js+.net實現一個基於X25519的AES_128_GCM的證書。
引用js插件
https://github.com/brix/crypto-js sha512.min.js 主要用於原始密碼加密。
https://github.com/bitwiseshiftleft/sjcl sjcl.min.js 須要從新壓縮一下,主要用於aes-gcm加密。
https://github.com/gimer/curve25519nxt curve.js 可從新壓縮一下,主要用於X25519的密鑰交換。
前端代碼:
function login() {
//聲明一個橢圓曲線對象
var curve = new Curve25519();
//隨機一個64位的hex數值,並轉化爲字節類型,作爲x25519的私鑰 var privateKey = new Key25519(hexToBytes(randomWord(64)));
//獲得橢圓曲線的公鑰 var publickey = curve.genPub(privateKey).key;
//把前端的私鑰傳入到後端,後端計算得出公用的aes加密的密鑰,同時獲得後端的公鑰 $.post("/Login/GetX25519PublicKey", { publicKey: bytesToHex(publickey) }, function (data) {
//獲得後端的公鑰,結合前端的私鑰,計算得出前端aes加密的密鑰(先後端計算的密鑰是一致的) var shareKey = bytesToHex(curve.genShared(privateKey, new Key25519(hexToBytes(data.PublicKey))).key);
//聲明一個登陸對象 var loginUser = new Object();
//用戶名 loginUser.UserName = $.trim($("#txtUserName").val());
//密碼兩次sha384加密
loginUser.Password = sha384(sha384($.trim($("#txtPWD").val())));
//驗證碼
loginUser.ValidateCode = $.trim($("#txtValidateCode").val());
//使用公用密鑰shareKey進行ase加密(aes後邊的參數介紹下mode有gcm和ccm模式,這裏用gcm模式;ts長度gcm模式是128;iter輪詢次數默認10000,我們改成1000,10000次數太多了會卡頓;salt鹽隨機數,iv是向量) var aesData = sjcl.encrypt(shareKey, JSON.stringify(loginUser), { mode: 'gcm', ts: 128, iter: 1000, salt: sjcl.random.randomWords(4), iv: sjcl.random.randomWords(3) });
//傳入加密後的login字符串,同時須要傳入後端返回的key(用於後端方便查找對應的公用密鑰) $.post("/Login/UserLogin", { loginUser: aesData, key: data.Key }, function (data) { if (data.Result == false)
{
alert("登陸失敗") } else { window.location = "/"; } }); }); }
//隨機必定長度的hex數值 function randomWord(len) { var str = "", arr = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f']; for (var i = 0; i < len; i++) { var pos = Math.round(Math.random() * (arr.length - 1)); str += arr[pos]; } return str; }
後端代碼(傳入前端的公鑰,生成aes公用密鑰,返回後端的公鑰):
//後端獲得前端傳過來的公鑰,生成aes公用密鑰,使用的是 BouncyCastle 類庫 public static Dictionary<string, string> GenerateX25519Keys(string publicKey) { SecureRandom secureRandom = new SecureRandom(); byte[] privateByte = new byte[X25519.ScalarSize]; byte[] publicByte = new byte[X25519.PointSize]; byte[] shareByte = new byte[X25519.PointSize]; secureRandom.NextBytes(privateByte); X25519.ScalarMultBase(privateByte, 0, publicByte, 0); X25519.ScalarMult(privateByte, 0, Hex.Decode(publicKey), 0, shareByte, 0); Dictionary<string, string> dc = new Dictionary<string, string>(); //生成x25519 hex公鑰 dc.Add("PublicKey", Hex.ToHexString(publicByte)); //生成x25519 hex私鑰 dc.Add("PrivateKey", Hex.ToHexString(privateByte)); //生成aes公用密鑰 dc.Add("ShareKey", Hex.ToHexString(shareByte)); //返回一個dictionary對象 return dc; } public ActionResult GetX25519PublicKey(string publicKey) { //獲得x25519key Dictionary<string, string> eccKeys = GenerateX25519Keys(publicKey);
//Redis NewtonsoftSerializer serializer = new NewtonsoftSerializer(); RedisConfiguration redisConfiguration = RedisCachingSectionHandler.GetConfig(); IRedisCacheConnectionPoolManager connectionPoolManager = new RedisCacheConnectionPoolManager(redisConfiguration); IRedisCacheClient redisClient = new RedisCacheClient(connectionPoolManager, serializer, redisConfiguration);
//使用guid key string key = Guid.NewGuid().ToString("n") + Guid.NewGuid().ToString("n");
//把x25519字典用redis緩存起來 redisClient.GetDbFromConfiguration().Add(key, eccKeys, DateTime.Now.AddSeconds(30));
//返回guid key和x25519 公鑰 return Json(new { Key = key, PublicKey = eccKeys["PublicKey"] }); }
後端代碼(解密前端傳過來的aes密文):
//根據aes公用key 解密前端傳進來的aes密文,使用的是 BouncyCastle 類庫
//傳進來的密文 public static string DecryptString(string ciphertext, string key) { //把aes密文轉換爲json對象 JObject aesJObject = JObject.Parse(ciphertext); //把json對象的ct屬性轉換爲字節(ct就是加密後的密文) byte[] ciphertextByte = Convert.FromBase64String(aesJObject["ct"].ToString()); //把json的salt屬性轉換爲字節 byte[] salt = Convert.FromBase64String(aesJObject["salt"].ToString()); //把json的iv屬性轉換爲字節 byte[] iv = Convert.FromBase64String(aesJObject["iv"].ToString()); //聲明一個PBKDF2對象 Pkcs5S2ParametersGenerator pbkdf2 = new Pkcs5S2ParametersGenerator(new Sha256Digest()); //根據aes公用key,salt,iter輪詢次數(前面傳進來的是1000,這裏也是1000)初始化PBKDF2對象 pbkdf2.Init(Encoding.UTF8.GetBytes(key), salt, 1000); byte[] keyByte = ((KeyParameter)pbkdf2.GenerateDerivedMacParameters(16 * 8)).GetKey(); // 解密獲得字符串 return Encoding.UTF8.GetString(Decrypt(ciphertextByte, keyByte, iv)); } //解密aes密文 public static byte[] Decrypt(byte[] ciphertext, byte[] key, byte[] iv) { //聲明aes gcm對象 GcmBlockCipher cipher = new GcmBlockCipher(new AesEngine()); KeyParameter keyParam = ParameterUtilities.CreateKeyParameter("AES", key); //根據key和IV初始化 ParametersWithIV cipherParameters = new ParametersWithIV(keyParam, iv); cipher.Init(false, cipherParameters); //解密 byte[] plaintext = new byte[cipher.GetOutputSize(ciphertext.Length)]; int length = cipher.ProcessBytes(ciphertext, 0, ciphertext.Length, plaintext, 0); cipher.DoFinal(plaintext, length); //返還前端加密前的login字節 return plaintext; } } //解密aes login密文 public ActionResult UserLogin(string loginUser, string key) {
try { //Redis相關 NewtonsoftSerializer serializer = new NewtonsoftSerializer(); RedisConfiguration redisConfiguration = RedisCachingSectionHandler.GetConfig(); IRedisCacheConnectionPoolManager connectionPoolManager = new RedisCacheConnectionPoolManager(redisConfiguration); IRedisCacheClient redisClient = new RedisCacheClient(connectionPoolManager, serializer, redisConfiguration); //使用傳進來的來 Key獲取Redis緩存的x25519 Dictionary Dictionary<string, string> eccKey = redisClient.GetDbFromConfiguration().Get<Dictionary<string, string>>(key); if (eccKey == null) {return; } //移除redis中x25519 redisClient.GetDbFromConfiguration().Remove(key);
//解密獲得前端的login對象 LoginViewModel loginUserView = JsonConvert.DeserializeObject<LoginViewModel>(DecryptString(loginUser, eccKey["ShareKey"])); //業務邏輯 // ... return Json(new { Result = true }); } catch (Exception ex) { logError.Error(ex);
return ; } }
這樣一個還算完整的登陸就算完成了,有興趣的能夠本身敲敲代碼,作個小測試。
PS:客官有時間光臨個人小站 萬倉網。