一、理論部分
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。