1. 程式人生 > >ASP.NET MVC中使用JS實現不對稱加密密碼傳輸

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 Application


1.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