ASP.NET MVC中使用JS實現不對稱加密密碼傳輸
摘要:ASP.NET MVC中登入頁面中點選登入後,使用者名稱、密碼將被明文傳輸到Controller中,使用Fiddler等工具可以輕鬆截獲並獲取密碼, 這是不安全的。 使用對稱加密,如AES,金鑰將被暴露前端程式碼,也是不安全的。使用不對稱加密能夠較好解決這個問題。本文以RSA不對稱加密的形式,在JS端通過公鑰對密碼進行加密,將密文傳輸到後端後通過金鑰進行解密。
關鍵字: 不對稱加密;對稱加密;RSA 演算法;AES; 金鑰;公鑰
0 背景
登入是最常見的需求之一,在這個環節,安全問題不可避免,明文傳輸很容易被截獲並暴露密碼原文。如下圖使用Fiddle中出現的情況。
為了避免這種情況,通常辦法有1 使用HTTPS形式解決; 2 使用公鑰和不對稱加密對密文進行加密;3使用對稱加密,比如AES。
這3種方案中,方案1是終極方案,但是需要克服證書獲取和配置的問題, 本方案不是本文討論重點,請有興趣的自行查閱https://letsencrypt.org/。方案3, 以AES加密為例,必須把加密金鑰存放在前端。 而前端對使用者來說是開源的,很多開發者嘗試把金鑰藏的路徑很深,但無疑這還是自欺欺人的。
方案2中,在JS端進行密碼的RSA加密是有必要的,因為密碼需要在使用者點選“登入”按鈕後被提交到伺服器,這個過程被截獲是很容易的。同時,防範CSRF型別攻擊的特性也必須保留。這就要求:必須使用AJAX在JS端對密碼加密,並向後臺的AccountController中的LoginAction發起Post請求。而不能使用傳統的FormSubmit的方案。
AJAX post請求中,需要注意問題: 由於需要防範CSRF攻擊的同時保障密文傳輸安全。需要同時顧及如下問題
問題1: 如何通過AJAX向Controller發起ajax請求?
問題2:如何在ajax請求中加入AntiForgeryToken?
問題3: AJAX請求前,如何對密碼進行RSA加密?
問題4: RSA的key format 有兩種, pem格式和C#所支援的XML格式,通常JS支援pem, C#支援xml, 如何轉換?
帶著問題,進入操作步驟;
1 操作步驟
1.1 新建Web Application1.2 選擇MVC, Authentication中選擇”IndividualUser Accounts”
1.3 Views-> Account->Login.cshtml中程式碼修改如下
需要將目標URL設定為隱藏欄位,供JS讀取
@model WebApplication_JS_RSA.ViewModels.LoginViewModel
@{
ViewBag.Title = "Log in";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<div class="wrapper--login">
<div class="wrapper--login__body">
<h1>Login</h1>
@using (@Html.BeginForm("Login", "Account", FormMethod.Post, new { id = "loginForm" }))
{
<div id="AccountLoginURL" class="hidden" data-url="@Url.Action("login", "account")"></div>
@Html.AntiForgeryToken()
<div class="val">
@Html.LabelFor(m => m.UserName)
<div class="val__field">
@Html.TextBoxFor(model => model.UserName, new { @class = "form-control", id = "userNameTextBox" })
@Html.ValidationMessageFor(model => model.UserName)
</div>
</div>
<div class="val">
@Html.LabelFor(m => Model.EncryptedPassword)
<div class="val__field">
@Html.PasswordFor(model => model.EncryptedPassword, new { @class = "form-control", id = "passwordTextBox" })
@Html.ValidationMessageFor(model => model.EncryptedPassword)
</div>
</div>
@Html.HiddenFor(model => model.ReturnUrl)
@Html.HiddenFor(model => model.RedirectDomain)
<div class="val__message" id="errorMsg"></div>
<div class="row">
<input type="button" id="LoginButton" value="Login" class="button--primary">
</div>
}
<div id="PublicKey" class="hidden" data-val="@Model.PublicKey"></div>
<div class="row line">
<span class="line__1"></span>or<span class="line__2"></span>
</div>
<div class="row">
<a href="@Url.Action("forgotpassword", "Account")" class="button--secondary">Forgot Password ?</a>
</div>
</div>
</div>
@section Scripts {
@Scripts.Render("~/Scripts/jquery-1.10.2.min.js")
@Scripts.Render("~/Scripts/jquery.validate.min.js")
@Scripts.Render("~/bundles/jqueryval")
@Scripts.Render("~/Scripts/jsencrypt.js")
@Scripts.Render("~/Scripts/Views/Account/Login.js")
}
1.4 Scripts資料夾下新增 Login.js, jsencrypt.js檔案
jsencrypt.js檔案請從“參考連結3”中獲取. Login.js程式碼如下:
var login = (
function ($) {
$(document).ready(
function () {
$('#LoginButton').click(function () {
var publicKey = $('#PublicKey').data("val");
var plainpassword = $('#passwordTextBox').val();
var AccountLoginURL = $('#AccountLoginURL').data("url");
var encryptedPassword;
var formSelector = "#loginForm";
var form = $(formSelector);
form.validate();
var isFormValid = form.valid();
//encrypt password
if (plainpassword !== null && plainpassword !== "") {
var crypt = new JSEncrypt();
crypt.setPublicKey(publicKey);
encryptedPassword = crypt.encrypt(plainpassword);
console.log(encryptedPassword);
$('#passwordTextBox').val(encryptedPassword);
}
debugger;
if (isFormValid) {
//blockUI
//showSpinner();
$.ajax({
type: "POST",
url: AccountLoginURL,
data: form.serialize(),
success: function (data, textStatus, jqXHR) {
if (data.RedirectUrl !== null) {
window.location.href = data.RedirectUrl;
}
else {
$('#errorMsg').text(data.ErrorMessage);
}
},
error: function (jqXhr, textStatus, errorThrown) {
console.log('error: ' + jqXhr.responseText);
},
complete: function (jqXHR, textStatus) {
//hideSpinner();
}
});
}
});
});
}(jQuery));
1.5 配置publicKey和PrivateKey
通過“3參考連結“中連線2,生成公鑰和私鑰。 公鑰保持pem格式,因為JS類庫使用的需要。 把私鑰通過“3參考連結“中連線4(轉換器)轉換成XML格式,因為.NET能夠識別XML格式的私鑰。
1.6 Controller端
Controllers->AccountController->Login()
// POST: /Account/Login
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login()
{
var form = Request.Form;
string plainTextPassword = form["plainTextPassword"].ToString();
string encryptedPassword = form["encryptedPassword"].ToString();
//decrypt
String privateKeyPathFile = AppDomain.CurrentDomain.BaseDirectory + @"\Content\PrivateKey.xml";
string RSAprivateKey = System.IO.File.ReadAllText(privateKeyPathFile);
RSAEncryption rsaCryption = new RSAEncryption();
string decryptedPwd = rsaCryption.RSADecrypt(RSAprivateKey, encryptedPassword);
return View();
}
1.7 RSAEncryption
如需程式碼,請參考連結: https://github.com/memoryfraction/CommonUsedFunctions/tree/master/Encryption%26Decryption
2 小結
2.1 小結
本文提出了在ASP.NETMVC中,密碼傳輸安全問題,提出了3種可行解決方案。重點講述了RSA不對稱加密的實現方式,同時保留了微軟自帶的AntiForgeryToken, 以防止CSRF攻擊。達到了密文傳輸密碼的效果,即使被人截獲,也無法得知密碼明文。
作者知識和精力都有限,如有不足,歡迎指正。
2.2 補充
在更新版的3.1 範例程式碼中,更新使用了Form Serialization技術,優點: 可以直接對錶單序列化,傳輸到後端; 能夠使用C# Decoration驗證; 建議前端後端同時驗證, 雙保險; 相見程式碼;
3 參考連結
3 JSEncrypt的主頁:http://travistidwell.com/jsencrypt/
5 《什麼是CSRF攻擊,如何在ASP.NET MVC網站中阻止這種攻擊?》http://blog.csdn.net/fanrong1985/article/details/71701301