1、理論部分html
一、爲何要給密碼加鹽mysql
咱們在數據庫中存入的密碼通常不會是明文,都要通加MD5加密後存入,可是有些簡單的密碼加密後存入數據庫也不安全,全部咱們採用密碼+鹽再進行MD5加密存入數據庫中。算法
數據存儲形式以下:sql
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 以後的散列,壞人們的數據字典已經沒法直接匹配,明文密碼被破解出來的機率也大大下降。安全
二、爲何要加隨機數session
當咱們在瀏覽器中輸入密碼後,雖然這個密碼被加密了,但要是被別人偵聽到了,用一樣的密碼去請求仍是會截獲到請求的數據。dom
此時咱們就須要針對不一樣的用戶生成隨機數,再給密碼加密。而後後臺再經過這個隨機數進行解密。
2、實踐
一、這裏咱們用的.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; } }
二、前臺經過隱藏標籤來存這個隨機數,還有展現密碼和用戶名輸入框。
<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>
三、用戶輸入用戶名和密碼後點擊登陸首先會去數據中查這個用戶的密碼鹽,這裏前臺頁面已經有了用戶輸入的密碼、隨機數和密碼鹽,這裏就能夠對密碼時行加密後傳輸了,代碼以下
$(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); } }) }); } }); });
四、數據提交到後臺再進行處理
[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); }
五、若是後臺登陸驗證都經過了咱們會返回到登陸首頁,在第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; }
校驗經過後會將主頁呈現給用戶。
六、用戶登出的代碼以下:
/// <summary> /// 退出登陸 /// </summary> public void signOut() { _httpContextAccessor.HttpContext.SignOutAsync(CookieAdminAuthInfo.AuthenticationScheme); }
到此,整個登陸模塊就完成了。