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