一、理论部分

1、为什么要给密码加盐

我们在数据库中存入的密码一般不会是明文,都要通加MD5加密后存入,但是有些简单的密码加密后存入数据库也不安全,所有我们采用密码+盐再进行MD5加密存入数据库中。

数据存储形式如下:

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

密码盐Salt 可以是任意字母、数字、或是字母或数字的组合,但必须是随机产生的,每个用户的 Salt 都不一样,

用户注册的时候,数据库中存入的不是明文密码,也不是简单的对明文密码进行散列,而是 MD5( 明文密码 + Salt),

也就是说,当用户登陆的时候,同样用这种算法验证。

MD5('' + '1ck12b13k1jmjxrg1h0129h2lj') = '6c22ef52be70e11b6f3bcf0f672c96ce'
MD5('' + '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 + ) + "分钟。", null, null);
}
} var md5Password = EncryptorHelper.GetMD5(user.Password + r);
//匹配密码
if (password.Equals(md5Password, StringComparison.InvariantCultureIgnoreCase))
{
user.LoginLock = false;
user.LoginFailedNum = ;
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()
};
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 > )
{
user.LoginLock = true;
user.AllowLoginTime = DateTime.Now.AddHours();
}
_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());
//缓存
_memoryCache.Set(String.Format(MODEL_KEY, tokenItem.SysUserId), tokenItem.SysUser, DateTimeOffset.Now.AddHours());
return tokenItem.SysUser;
}
}
return null;
}

校验通过后会将主页呈现给用户。

6、用户登出的代码如下:

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

到此,整个登录模块就完成了。

打个广告:如果你喜欢这篇文章的话,有需求微信大量投票或点赞的朋友可以给我介绍哦,QQ:3282079595。

 

.NetCore 登录(密码盐+随机数)的更多相关文章

  1. HTTP协议下保证登录密码不被获取最健壮方式

    原文:http://www.cnblogs.com/intsmaze/p/6009648.html HTTP协议下保证登录密码不被获取最健壮方式   说到在http协议下用户登录如何保证密码安全这个问 ...

  2. C# 密码盐码加密

    每次新建账号密码的时候都需要获取一下新的盐码,之后用使用MD5为用户密码加密 /// <summary> /// 获取新的密码盐码 /// </summary> /// < ...

  3. 利用 John the Ripper 破解用户登录密码

    一.什么是 John the Ripper ? 看到这个标题,想必大家都很好奇,John the Ripper 是个什么东西呢?如果直译其名字的话就是: John 的撕裂者(工具). 相比大家都会觉得 ...

  4. MySQL 用户登录密码和远程登录权限问题

    1.mysql数据库,忘记root用户登录密码. 解决如下: a.重置密码 #/etc/init.d/mysqld stop #mysqld_safe --user=mysql --skip-gran ...

  5. 家庭路由器设置以及win10链接无线不显示登录密码 直接提示链接出错问题解决

    家庭路由器设置 网线插入WAN口,用网客户端接在LAN口,就是路由器模式 LAN→WAN设置:电脑→第二个路由器LAN→进入设置界面: 网络参数→WAN口设置→WAN口连接类型→动态IP→保存. 网络 ...

  6. WordPress忘记登录密码

    后台的登录密码使用的是md5加密的,有时候会忘记登录密码,那么可以修改数据库,把密码改为你知道的字符串的md5加密值 如 hello对应的md5加密值为:5d41402abc4b2a76b9719d9 ...

  7. 使用BCrypt算法加密存储登录密码用法及好处

    //导入import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; /** *使用BCrypt算法加密存储登录密码 ...

  8. win10怎么取消登录密码

    win10安装后每次登录都需要输入密码,挺烦的,查了下,原来windows10无密码登录设置挺方便. 1. 按下win+x组合键,如下图所示 2. 在弹出菜单选择运行,如下图所示 或者直接按win+r ...

  9. Client默认用户及登录密码(转)

    Client默认用户及登录密码 SAP系统(如ERP.CRM等)安装完成,初始化状态下有若干个客户端(Client).如果是生产系统,一般只有000.001.066等三个Client:如果是IDES系 ...

随机推荐

  1. LQR算法如何跟随变化的期望状态

    开门见山,通过LQR的能量函数可以看出,LQR算法是将状态量控制到零(关于能量函数请看我的随笔:LQR要点),但实际控制中我们希望状态量能够跟随期望值 下面将会解决如何用LQR算法跟随变化的期望值: ...

  2. SpringBoot下Druid连接池的使用配置

    Druid是一个JDBC组件,druid 是阿里开源在 github 上面的数据库连接池,它包括三部分: * DruidDriver 代理Driver,能够提供基于Filter-Chain模式的插件体 ...

  3. emacs-显示行号以及跳转到指定行

    [显示行号]M+x display-line-number-mode <Return> [跳转行号]M+x goto-line 然后输入你想跳转到的行号 <Return> (在 ...

  4. 优化:在k8s上部署的gitlab

    gitlab组件图 gitlab在k8s上占用资源 # kubectl top pods -n default | grep git* gitlab-gitaly-0 9m 444Mi gitlab- ...

  5. VMware实现宿主机和虚拟机处于同一网段

    打开虚拟网络编辑器 选择VMnet0桥接模式,在VMnet信息中,选择可以选择的网卡,然后保存. 打开虚拟机设置,在“硬件”选项卡的网络适配器中选择桥接模式即可.

  6. 【线型DP】【LCS】UVA_10635 Prince and Princess

    嘤嘤嘤,我又来了,刚A完就写,这个沙雕题有丶恶心.                  ???时间4.11发现所有表情包都莫得了 题目: In an n×n chessboard, Prince and ...

  7. efcore 跨表查询,实现一个接口内查询两个不同数据库里各自的表数据

    最近有efcore跨库查询的需求,研究了下colder框架里文档的分库实现,发现并不能完全实现一个接口下的跨库查询请求,只能满足一个业务层构造指定的唯一一个数据库访问接口. 先说下文档是怎么实现的 D ...

  8. 记一次WPF程序带参数启动

    问题:总共有两个程序.第一个程序使用Process带参数启动第二个程序. 网上一堆人都说什么重写Main入口啊 什么的.然后还一堆人跟着复制发文章.我也是醉了,简直是坑人.为何要舍近求远,直接重写AP ...

  9. Maven 专题(六):Maven核心概念详解(二)

    5 仓库 5.1 分类 [1]本地仓库:为当前本机电脑上的所有 Maven 工程服务.[2]远程仓库:        (1)私服:架设在当前局域网环境下,为当前局域网范围内的所有 Maven 工程服务 ...

  10. JVM 专题十三:运行时数据区(八)直接内存

    1. 直接内存 不是虚拟机运行时数据区的一部分,也不是<Java虚拟机规范>中定义的内存区域. 直接内存是Java堆外的.直接向系统申请的内存区间. 来源于NIO,通过存在堆中的Direc ...