如果 5 次尝试失败,则锁定用户
本文关键字:锁定 用户 失败 如果 | 更新日期: 2023-09-27 17:56:57
我已经可以减少数据库中设置为 5
的AttemptsLeft
。如果我使用正确的用户名错误地输入密码,AttemptsLeft
将减少 1。如果用户的状态为Suspended
,我也已经可以让用户不登录系统。
问题:
我设置当AttemptsLeft
降低到0时,用户的状态将变为Suspended
。但是,数据库中的AttemptsLeft
总是会降低,用户的状态不会变得Suspended
,但是如果我正确输入用户名和密码,无论AttemptsLeft
还剩多少,它都会Suspended
。
可能出了什么问题?
我认为从数据库中检索的user.Attempts
不起作用,那是因为user.Attempts
始终0
,只有数据库中的AttemptsLeft
会减少,因为我正在减少它query
。
这是我正在使用的代码:
用户管理器类:
public void GetAttempts(string Username, string Password)
{
LoginContext context = new LoginContext();
using (SqlConnection conn = new SqlConnection(connectionString))
{
string query = "SELECT [Username], [Password], [AttemptsLeft] FROM [Information] WHERE [Username] = @Username AND [Password] = @Password";
conn.Open();
using (SqlCommand cmd = new SqlCommand(query, conn))
{
cmd.Parameters.Add("@Username", SqlDbType.NVarChar);
cmd.Parameters["@Username"].Value = Username;
cmd.Parameters.Add("@Password", SqlDbType.NVarChar);
cmd.Parameters["@Password"].Value = Password;
using (SqlDataReader reader = cmd.ExecuteReader())
{
while (reader.Read())
{
context.Attempts = Convert.ToInt32(reader["AttemptsLeft"]);
}
}
}
conn.Close();
}
}
public bool CheckUser(string Username, string Password)
{
using (SqlConnection conn = new SqlConnection(connectionString))
{
_query = "SELECT [Username], [Password] FROM [Information] WHERE [Username] = @Username AND [Password] = @Password";
conn.Open();
using (SqlCommand cmd = new SqlCommand(_query, conn))
{
cmd.Parameters.Add("@Username", SqlDbType.NVarChar);
cmd.Parameters["@Username"].Value = Username;
cmd.Parameters.Add("@Password", SqlDbType.NVarChar);
cmd.Parameters["@Password"].Value = Password;
using (SqlDataReader reader = cmd.ExecuteReader())
{
if (reader.HasRows)
{
return true;
}
else
{
return false;
}
}
}
}
}
public bool CheckStatus(string Username, string Password)
{
using (SqlConnection conn = new SqlConnection(connectionString))
{
_query = "SELECT [Username], [CurrentStatus] FROM [Information] WHERE [Username] = @Username AND [CurrentStatus] = @CurrentStatus";
conn.Open();
using (SqlCommand cmd = new SqlCommand(_query, conn))
{
cmd.Parameters.Add("@Username", SqlDbType.NVarChar);
cmd.Parameters["@Username"].Value = Username;
cmd.Parameters.Add("@CurrentStatus", SqlDbType.NVarChar);
cmd.Parameters["@CurrentStatus"].Value = "Active";
using (SqlDataReader reader = cmd.ExecuteReader())
{
if (reader.HasRows)
{
return true;
}
else
{
return false;
}
}
}
}
}
public bool SuspendUser(string Username)
{
bool flag = false;
using (SqlConnection conn = new SqlConnection(connectionString))
{
string query = "SELECT [Username], [CurrentStatus] FROM [Information] WHERE [Username] = @Username";
string _query = "UPDATE [Information] SET [CurrentStatus] = @CurrentStatus WHERE [Username] = @Username";
conn.Open();
using (SqlCommand cmd = new SqlCommand(query, conn))
using (SqlCommand _cmd = new SqlCommand(_query, conn))
{
cmd.Parameters.Add("@Username", SqlDbType.NVarChar);
cmd.Parameters["@Username"].Value = Username;
_cmd.Parameters.Add("@Username", SqlDbType.NVarChar);
_cmd.Parameters["@Username"].Value = Username;
_cmd.Parameters.Add("@CurrentStatus", SqlDbType.NVarChar);
_cmd.Parameters["@CurrentStatus"].Value = "Suspended";
flag = Convert.ToBoolean(_cmd.ExecuteNonQuery());
}
conn.Close();
}
return flag;
}
public bool DecreaseAttempts(string Username)
{
bool flag = false;
using (SqlConnection conn = new SqlConnection(connectionString))
{
string query = "SELECT [Username], [AttemptsLeft] FROM [Information] WHERE [Username] = @Username";
string _query = "UPDATE [Information] SET [AttemptsLeft] = [AttemptsLeft] - 1 WHERE [Username] = @Username";
conn.Open();
using (SqlCommand cmd = new SqlCommand(query, conn))
using (SqlCommand _cmd = new SqlCommand(_query, conn))
{
cmd.Parameters.Add("@Username", SqlDbType.NVarChar);
cmd.Parameters["@Username"].Value = Username;
_cmd.Parameters.Add("@Username", SqlDbType.NVarChar);
_cmd.Parameters["@Username"].Value = Username;
flag = Convert.ToBoolean(_cmd.ExecuteNonQuery());
}
conn.Close();
}
return flag;
}
登录上下文模型:
public int Attempts
{
get;
set;
}
控制器:
UserManager manager = new UserManager();
[HttpGet]
public ActionResult Login()
{
return View();
}
[HttpPost]
public ActionResult Login(LoginContext user, string Username, string Password)
{
if (!Request.IsAuthenticated)
{
if (ModelState.IsValid)
{
// `if` statement below will gets executed when I enter the password and username correctly.
if (manager.CheckUser(Username, Password))
{
// `if` statement below will gets executed when user's status is not `Suspended`
if (manager.CheckStatus(Username, Password))
{
manager.GetAttempts(Username, Password);
// `if` statement below will not gets executed. It is like `user.Attempts` always be `0`.
if (user.Attempts > 0)
{
FormsAuthentication.SetAuthCookie(user.Username, false);
return RedirectToAction("List", "Home");
}
//`else` statement below will gets executed whenever I enter the username and password correctly.
else
{
ModelState.AddModelError(string.Empty, "The account: " + Username + ", has been locked due too many failed login attempts!");
manager.SuspendUser(Username);
}
}
//`else` statement below will gets executed when user's status is `Suspended`.
else
{
ModelState.AddModelError(string.Empty, "Your account has been locked due too many failed login attempts!");
}
}
// `else` statement below will gets executed when I enter the password wrongly and username correctly or vice versa.
else
{
ModelState.AddModelError(string.Empty, "Username or password incorrect!");
manager.DecreaseAttempts(Username);
}
}
return View(user);
}
}
如果您遵循您的方案,即当用户无效时,它只会减少您的无效尝试。除非用户正确输入登录凭据,否则它实际上不会挂起用户。
实际上,您的sudo逻辑应该去:
- 验证凭据
- 凭据有效
- 检查帐户状态,被暂停,然后拒绝访问
- 将尝试重置回 5
- 允许用户进入应用程序
- 凭据无效
- 将尝试次数减少一次
- 如果需要,将帐户设置为已暂停
- 凭据有效
很多这样的逻辑实际上可以绑定到一两个方法中。同样在DecreaseAttempts()
方法中,您有两个 SQL 命令,其中永远不会执行 sql 命令cmd
下面是一个在返回状态枚举的方法中执行此操作的示例。现在这是一个非常基本的示例,但只需要一种方法来执行您的完全授权方法。我已经注释了代码。
public partial class UserManager
{
const int MaxAttempts = 5;
public LoginStatus ValidateUser(string username, string password)
{
if (string.IsNullOrWhiteSpace(username))
throw new ArgumentNullException("username");
//set the password to empty if it is null
password = password ?? "";
//create the connection
using (var connection = new SqlConnection(Configuration.ConnectionString))
{
//assign some local variables
int attemptsLeft = MaxAttempts;
string currentStatus = "Active";
string userPassword = null;
//get the information for the user, only query by username so we have all the data. We will match the password later on
string query = "SELECT TOP(1) [Username], [Password], [AttemptsLeft], [CurrentStatus] FROM [Information] WHERE Username = @username";
using (var command = new SqlCommand(query, connection))
{
command.Parameters.AddWithValue("@username", username);
command.CommandType = System.Data.CommandType.Text;
connection.Open();
using (var reader = command.ExecuteReader())
{
//no rows.. Invalid username
if (!reader.HasRows)
{
connection.Close();
return LoginStatus.InvalidCredentials;
}
//read the first row (hence the break)
while (reader.Read())
{
attemptsLeft = (int)reader["AttemptsLeft"];
currentStatus = (string)reader["CurrentStatus"];
userPassword = (string)reader["Password"];
break;
}
reader.Close();
}
connection.Close();
}
//if the account is suspended then dont even bother with password checking
if (currentStatus.Equals("Suspended", StringComparison.CurrentCultureIgnoreCase))
{
return LoginStatus.Suspended;
}
//invalid password lets handle the invalid credentials logic
if (!password.Equals(userPassword))
{
attemptsLeft -= 1;
//decrease the attempts, lets just stop at zero as we dont need negative attempts
if(attemptsLeft >= 0)
{
query = "UPDATE [Information] SET [AttemptsLeft] = @attemptsLeft WHERE Username = @username";
using (var command = new SqlCommand(query, connection))
{
command.Parameters.AddWithValue("@username", username);
command.Parameters.AddWithValue("@attemptsLeft", attemptsLeft);
connection.Open();
command.ExecuteNonQuery();
connection.Close();
}
}
//suspend the account when attempts less than or equal to zero
if (attemptsLeft <= 0)
{
query = "UPDATE [Information] SET [CurrentStatus] = @currentStatus WHERE Username = @username";
using (var command = new SqlCommand(query, connection))
{
command.Parameters.AddWithValue("@username", username);
command.Parameters.AddWithValue("@currentStatus", "Suspended");
connection.Open();
command.ExecuteNonQuery();
connection.Close();
}
//exit method as login account suspended
return LoginStatus.Suspended;
}
//exit as invalid login credentials
return LoginStatus.InvalidCredentials;
}
//if we are here lets quickly reset the login attempts back to 5, and account status to active as this is a valid login
query = "UPDATE [Information] SET [AttemptsLeft] = @attemptsLeft, [CurrentStatus] = @currentStatus WHERE Username = @username";
using (var command = new SqlCommand(query, connection))
{
command.Parameters.AddWithValue("@username", username);
command.Parameters.AddWithValue("@attemptsLeft", MaxAttempts);
command.Parameters.AddWithValue("@currentStatus", "Active");
connection.Open();
command.ExecuteNonQuery();
connection.Close();
}
//if we got here then every thing is a match
return LoginStatus.Authorized;
}
}
}
public enum LoginStatus
{
Authorized,
InvalidCredentials,
Suspended
}
要使用此功能,可以像下面一样简单(请注意,您必须更改视图重定向)
[HttpPost]
public ActionResult Index(string username, string password)
{
if(string.IsNullOrWhiteSpace(username))
{
this.ModelState.AddModelError("", "Invalid Login Credential. No username sent.");
return View();
}
var manager = new UserManager();
var result = manager.ValidateUser(username, password);
switch (result)
{
case LoginStatus.Authorized:
return RedirectToAction("About", "Home");
case LoginStatus.InvalidCredentials:
this.ModelState.AddModelError("", "Invalid Login Credentials. Username or password incorrect");
break;
case LoginStatus.Suspended:
this.ModelState.AddModelError("", "Account Suspeneded");
break;
}
return View();
}
只是为了好玩,我把它重写成一个简单的存储过程。
CREATE PROCEDURE ValidateUser
@username nvarchar(50),
@password nvarchar(50)
AS
BEGIN
SET NOCOUNT ON;
DECLARE @userPassword nvarchar(50) = NULL
DECLARE @maxAttempts int = 5
DECLARE @attemptsLeft int = 5
DECLARE @currentStatus nvarchar(50)
/*
RETURN CODES:
0 = Authorized
1 = InvalidCredentials
2 = Suspended
*/
SELECT TOP(1) @userPassword = [UserName], @attemptsLeft = [AttemptsLeft], @currentStatus = [CurrentStatus] FROM [Information] WHERE UserName = @username
IF @userPassword IS NULL
BEGIN
SELECT 1 as [Result], @maxAttempts as [AttemptsRemaining]
RETURN
END
--account suspended.. Return a suspended result
If @currentStatus = 'Suspended'
BEGIN
SELECT 2 as [Result], 0 as [AttemptsRemaining]
RETURN
END
--passwords dont match (note this is case insensitive on default collation)
If @password IS NULL OR @password <> @userPassword
BEGIN
--decrease attempts
SET @attemptsLeft = @attemptsLeft - 1
--if the attempts left are greater than 0 then set the account active and decrease the attempts remaining
IF @attemptsLeft > 0
BEGIN
UPDATE [Information] SET [CurrentStatus] = 'Active', AttemptsLeft = @attemptsLeft WHERE UserName = @username
SELECT 1 as [Result], @attemptsLeft as [AttemptsRemaining]
RETURN
END
--else the attempts left are less than or equal to zero therefore they should be suspended and attempts left set to zero (dont want negative attempts)
ELSE
BEGIN
UPDATE [Information] SET [CurrentStatus] = 'Suspended', AttemptsLeft = 0 WHERE UserName = @username
SELECT 2 as [Result], 0 as [AttemptsRemaining]
RETURN
END
END
--if we get here then all is good and we can just reset the account status and max attempts for the next login attempt
UPDATE [Information] SET [CurrentStatus] = 'Active', AttemptsLeft = @maxAttempts WHERE UserName = @username
SELECT 0 as [Result], @maxAttempts AS [AttemptsRemaining]
END
GO
然后调用它非常简单(注意,我还将返回类型更改为返回状态和剩余尝试的调用。
方法
public LoginResult ValidateUserStoredProcedure(string username, string password)
{
if (string.IsNullOrWhiteSpace(username))
throw new ArgumentNullException("username");
//set the password to empty if it is null
password = password ?? "";
//create the connection
using (var connection = new SqlConnection(Configuration.ConnectionString))
{
var result = new LoginResult
{
AttemptsRemaining = 5,
Status = LoginStatus.InvalidCredentials
};
try
{
using (var command = new SqlCommand("EXEC ValidateUser @username, @password", connection))
{
command.Parameters.AddWithValue("@username", username);
command.Parameters.AddWithValue("@password", password);
command.CommandType = System.Data.CommandType.Text;
connection.Open();
using (var reader = command.ExecuteReader())
{
while (reader.Read())
{
result.Status = ((LoginStatus)(int)reader["Result"]);
result.AttemptsRemaining = (int)reader["AttemptsRemaining"];
break;
}
reader.Close();
}
connection.Close();
}
return result;
}
catch (Exception ex)
{
if (connection.State != System.Data.ConnectionState.Closed)
connection.Close();
Debug.WriteLine("Error on sql query:" + ex.Message);
return result;
}
}
}
结果类
public class LoginResult
{
public LoginStatus Status { get; set; }
public int AttemptsRemaining { get; set; }
}
public enum LoginStatus : int
{
Authorized = 0,
InvalidCredentials = 1,
Suspended = 2
}
控制器
[HttpPost]
public ActionResult Index(string username, string password)
{
if (string.IsNullOrWhiteSpace(username))
{
this.ModelState.AddModelError("", "Invalid Login Credential. No username sent.");
return View();
}
var manager = new UserManager();
var result = manager.ValidateUserStoredProcedure(username, password);
switch (result.Status)
{
case LoginStatus.Authorized:
return RedirectToAction("About", "Home");
case LoginStatus.InvalidCredentials:
if (result.AttemptsRemaining < 5)
this.ModelState.AddModelError("", "Invalid Login Credentials. Username or password incorrect. Attempts remaining:" + result.AttemptsRemaining);
else
this.ModelState.AddModelError("", "Invalid Login Credentials. Username or password incorrect.");
break;
case LoginStatus.Suspended:
this.ModelState.AddModelError("", "Account Suspeneded");
break;
}
return View();
}
如何优化取决于您,但这种授权级别相当弱。它还显示您将密码存储为纯文本。但这是另一个话题。