.NetCore 登入(密碼鹽+隨機數)

zhengwei_cq發表於2020-07-25

一、理論部分

1、為什麼要給密碼加鹽

我們在資料庫中存入的密碼一般不會是明文,都要通加MD5加密後存入,但是有些簡單的密碼加密後存入資料庫也不安全,所有我們採用密碼+鹽再進行MD5加密存入資料庫中。

資料儲存形式如下:

mysql> select * from User;
+----------+----------------------------+----------------------------------+
| UserName | Salt                       | PwdHash                          |
+----------+----------------------------+----------------------------------+
| lichao   | 1ck12b13k1jmjxrg1h0129h2lj | 6c22ef52be70e11b6f3bcf0f672c96ce |
| akasuna  | 1h029kh2lj11jmjxrg13k1c12b | 7128f587d88d6686974d6ef57c193628 |

密碼鹽Salt 可以是任意字母、數字、或是字母或數字的組合,但必須是隨機產生的,每個使用者的 Salt 都不一樣,

使用者註冊的時候,資料庫中存入的不是明文密碼,也不是簡單的對明文密碼進行雜湊,而是 MD5( 明文密碼 + Salt),

也就是說,當使用者登陸的時候,同樣用這種演算法驗證。

MD5('123' + '1ck12b13k1jmjxrg1h0129h2lj') = '6c22ef52be70e11b6f3bcf0f672c96ce'
MD5('456' + '1h029kh2lj11jmjxrg13k1c12b') = '7128f587d88d6686974d6ef57c193628'

由於加了 Salt,即便資料庫洩露了,但是由於密碼都是加了 Salt 之後的雜湊,壞人們的資料字典已經無法直接匹配,明文密碼被破解出來的概率也大大降低。

2、為什麼要加隨機數

當我們在瀏覽器中輸入密碼後,雖然這個密碼被加密了,但要是被別人偵聽到了,用同樣的密碼去請求還是會截獲到請求的資料。

此時我們就需要針對不同的使用者生成隨機數,再給密碼加密。然後後臺再通過這個隨機數進行解密。

二、實踐

1、這裡我們用的.NetCore MVC的形式,通過一個登入頁面的方法我們進行登入頁面,要進入登入的控制器中會生成一個隨機數,將這個隨機數存到session中,並將這個隨機數返回到前臺

private const string R_KEY = "R_KEY";
public
IActionResult LoginIndex() { string r = EncryptorHelper.GetMD5(Guid.NewGuid().ToString()); HttpContext.Session.SetString(R_KEY, r); LoginModel loginModel = new LoginModel() { R = r }; return View(loginModel); }

loginMode是一個返回到頁面的強型別檢視

   public class LoginModel
    {
        /// <summary>
        /// 賬號
        /// </summary>
        [Required(ErrorMessage = "請輸入賬號")]
        public string Account { get; set; }

        /// <summary>
        /// 密碼
        /// </summary>
        [Required(ErrorMessage = "請輸入密碼")]
        public string Password { get; set; }

        /// <summary>
        /// 
        /// </summary>
        public string R { get; set; }
    }

 

2、前臺通過隱藏標籤來存這個隨機數,還有展示密碼和使用者名稱輸入框。

    <form asp-route="adminLogin" method="post">
                                            <input type="hidden" id="r_random" value="@Model.R" />
                                            <fieldset>
                                                <label class="block clearfix">
                                                    <span class="block input-icon input-icon-right">
                                                        @Html.TextBoxFor(m => m.Account, new { @class = "form-control", placeholder = "使用者名稱" })
                                                        <i class="ace-icon fa fa-user"></i>
                                                    </span>
                                                </label>

                                                <label class="block clearfix">
                                                    <span class="block input-icon input-icon-right">
                                                        @Html.PasswordFor(m => m.Password, new { @class = "form-control", placeholder = "密碼" })
                                                        <i class="ace-icon fa fa-lock"></i>
                                                    </span>
                                                </label>

                                                <div class="space"></div>

                                                <div class="clearfix">
                                                    <label class="inline">
                                                        <input type="checkbox" id="RememberMe" name="RememberMe" value="true" class="ace" />
                                                        <span class="lbl"> 記住我</span>
                                                    </label>
                                                    <button type="button" id="myButton" data-loading-text="登入中..." class="width-35 pull-right btn btn-sm btn-primary">
                                                        <i class="ace-icon fa fa-key"></i>
                                                        <span class="bigger-110">登入</span>
                                                    </button>
                                                </div>
                                                <div class="space-4"></div>
                                            </fieldset>
                                        </form>

3、使用者輸入使用者名稱和密碼後點選登入首先會去資料中查這個使用者的密碼鹽,這裡前臺頁面已經有了使用者輸入的密碼、隨機數和密碼鹽,這裡就可以對密碼時行加密後傳輸了,程式碼如下

$(function () {
            $('#myButton').click(function () {
                if ($('form').valid()) {
                    var account = $('#Account').val();
                    var password = $('#Password').val();
                    var r = $('#r_random').val();
                    $.get('@Url.RouteUrl("getSalt")?account=' + account, function (salt) {
                        password = $.md5(password + salt);
                        password = $.md5(password + r);
                        $.post('@Url.RouteUrl("adminLogin")', { "Account": account, "Password": password }, function (data) {
                            if (data.status) {
                                $('#error_msg').html('登陸成功,正在進入系統...');
                                window.location.href = '@Url.RouteUrl("mainIndex")';
                            } else {
                                $('#error_msg').html(data.message);
                            }
                         })

                    });
                }
            });

        });

4、資料提交到後臺再進行處理

[HttpPost]
        [Route("login")]
        public IActionResult LoginIndex(LoginModel model)
        {
            string r = HttpContext.Session.GetString(R_KEY);
            r = r ?? "";
            if (!ModelState.IsValid)
            {
                AjaxData.Message = "請輸入使用者賬號和密碼";
                return Json(AjaxData);
            }
            var result = _sysUserService.validateUser(model.Account, model.Password, r);
            AjaxData.Status = result.Item1;
            AjaxData.Message = result.Item2;
            if (result.Item1)
            {
                _authenticationService.signIn(result.Item3, result.Item4.Name);
            }
            return Json(AjaxData);
        }
如果登入資訊沒有問題我們會呼叫_authenticationService.signIn方法來儲存登入狀態,也就是將token資訊和使用者名稱資訊存入:
   /// <summary>
        /// 儲存等狀態
        /// </summary>
        /// <param name="token"></param>
        /// <param name="name"></param>
        public void signIn(string token, string name)
        {
            ClaimsIdentity claimsIdentity = new ClaimsIdentity();
            claimsIdentity.AddClaim(new Claim(ClaimTypes.Sid, token));
            claimsIdentity.AddClaim(new Claim(ClaimTypes.Name, name));
            ClaimsPrincipal claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
            _httpContextAccessor.HttpContext.SignInAsync(CookieAdminAuthInfo.AuthenticationScheme, claimsPrincipal);

        }
在 _sysUserService.validateUser方法中我們將使用者名稱密碼還有隨機數再次傳入,驗證登入狀態,在這個方法中我們校驗了使用者是否被鎖,使用者登入日誌記錄、登入成功後寫入token表和密碼的匹配,

在進行密碼匹配時我們將使用者資料庫中的密碼和隨機數進行MD5加密後與使用者傳入的密碼進行匹配。程式碼如下:

  /// <summary>
        /// 驗證登入狀態
        /// </summary>
        /// <param name="account">登入賬號</param>
        /// <param name="password">登入密碼</param>
        /// <param name="r">登入隨機數</param>
        /// <returns></returns>
        public (bool Status, string Message, string Token, SysUser User) validateUser(string account, string password, string r)
        {
            var user = getByAccount(account);
            if (user == null)
                return (false, "使用者名稱或密碼錯誤", null, null);
            if (!user.Enabled)
                return (false, "你的賬號已被凍結", null, null);

            if (user.LoginLock)
            {
                if (user.AllowLoginTime > DateTime.Now)
                {
                    return (false, "賬號已被鎖定" + ((int)(user.AllowLoginTime - DateTime.Now).Value.TotalMinutes + 1) + "分鐘。", null, null);
                }
            }

            var md5Password = EncryptorHelper.GetMD5(user.Password + r);
            //匹配密碼
            if (password.Equals(md5Password, StringComparison.InvariantCultureIgnoreCase))
            {
                user.LoginLock = false;
                user.LoginFailedNum = 0;
                user.AllowLoginTime = null;
                user.LastLoginTime = DateTime.Now;
                user.LastIpAddress = "";

                //登入日誌
                user.SysUserLoginLogs.Add(new SysUserLoginLog()
                {
                    Id = Guid.NewGuid(),
                    IpAddress = "",
                    LoginTime = DateTime.Now,
                    Message = "登入:成功"
                });
                //單點登入,移除舊的登入token

                var userToken = new SysUserToken()
                {
                    Id = Guid.NewGuid(),
                    ExpireTime = DateTime.Now.AddDays(15)
                };
                user.SysUserTokens.Add(userToken);
                _sysUserRepository.DbContext.SaveChanges();
                return (true, "登入成功", userToken.Id.ToString(), user);
            }
            else
            {
                //登入日誌
                user.SysUserLoginLogs.Add(new SysUserLoginLog()
                {
                    Id = Guid.NewGuid(),
                    IpAddress = "",
                    LoginTime = DateTime.Now,
                    Message = "登入:密碼錯誤"
                });
                user.LoginFailedNum++;
                if (user.LoginFailedNum > 5)
                {
                    user.LoginLock = true;
                    user.AllowLoginTime = DateTime.Now.AddHours(2);
                }
                _sysUserRepository.DbContext.SaveChanges();
            }
            return (false, "使用者名稱或密碼錯誤", null, null);
        }

5、如果後臺登入驗證都通過了我們會返回到登入首頁,在第3步時  window.location.href = '@Url.RouteUrl("mainIndex")';

當然在進入這個首頁時會進行使用者身份校驗,我們把這個校驗寫在方法過濾器中吧,只要把這個過濾器標籤的都需求進行校驗使用者登入資訊,如果沒有使用者資訊就返回到登入首頁面。程式碼如下:

  /// <summary>
    /// 登入狀態過濾器
    /// </summary>
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
    public class AdminAuthFilter : Attribute, IResourceFilter
    { 
        public void OnResourceExecuted(ResourceExecutedContext context)
        {

        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="context"></param>
        public void OnResourceExecuting(ResourceExecutingContext context)
        {
           var _adminAuthService = EnginContext.Current.Resolve<IAdminAuthService>();
            var user = _adminAuthService.getCurrentUser();
            if (user == null || !user.Enabled)
                context.Result = new RedirectToRouteResult("adminLogin", new { returnUrl = context.HttpContext.Request.Path });
        }
    }
_adminAuthService.getCurrentUser(),在這個方法中我們拿到進求過來的tokenid,程式碼如下:
 /// <summary>
        /// 獲取當前登入使用者
        /// </summary>
        /// <returns></returns>
        public SysUser getCurrentUser()
        {
            var result = _httpContextAccessor.HttpContext.AuthenticateAsync(CookieAdminAuthInfo.AuthenticationScheme).Result;
            if (result.Principal == null)
                return null;
            var token = result.Principal.FindFirstValue(ClaimTypes.Sid);
            return _sysUserService.getLogged(token ?? "");
        }

拿到tokenId值後會呼叫_sysUserService.getLogged方法,在這個方法中我們通過tokenId獲取到了token.通過token獲取到了使用者資訊,再將使用者資訊返回,並將token資訊寫入到快取中

 /// <summary>
        /// 通過當前登入使用者的token 獲取使用者資訊,並快取
        /// </summary>
        /// <param name="token"></param>
        /// <returns></returns>
        public SysUser getLogged(string token)
        {
            SysUserToken userToken = null;
            SysUser sysUser = null; 

            _memoryCache.TryGetValue<SysUserToken>(token, out userToken);
            if (userToken!=null)
            {
                _memoryCache.TryGetValue(String.Format(MODEL_KEY, userToken.SysUserId), out sysUser);
            }
            if (sysUser != null)
                return sysUser;

            Guid tokenId = Guid.Empty;

            if (Guid.TryParse(token, out tokenId))
            {
                var tokenItem = _sysUserTokenRepository.Table.Include(x => x.SysUser)
                     .FirstOrDefault(o => o.Id == tokenId);
                if (tokenItem != null)
                {
                    _memoryCache.Set(token, tokenItem, DateTimeOffset.Now.AddHours(4));
                    //快取
                    _memoryCache.Set(String.Format(MODEL_KEY, tokenItem.SysUserId), tokenItem.SysUser, DateTimeOffset.Now.AddHours(4));
                    return tokenItem.SysUser;
                }
            }
            return null;
        }

校驗通過後會將主頁呈現給使用者。

6、使用者登出的程式碼如下:

/// <summary>
        /// 退出登入
        /// </summary>
        public void signOut()
        {
            _httpContextAccessor.HttpContext.SignOutAsync(CookieAdminAuthInfo.AuthenticationScheme);
        }

 

到此,整個登入模組就完成了。

打個廣告:如果你喜歡這篇文章的話,有需求微信大量投票或點讚的朋友可以給我介紹哦,QQ:3282079595。

 

 

相關文章