.NetCore 登录(密码盐+随机数)
一、理论部分
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。