Этап 3. Авторизация, регистрация и CAPTCHA¶
Схема авторизации¶
Авторизация — это проверка учетной записи перед открытием главного окна. Приложение не сравнивает введенный пароль с паролем из базы, потому что пароль в базе не хранится. Сравнивается хэш.
flowchart TD
A["Ввод логина и пароля"] --> B["Поиск пользователя по логину"]
B --> C{"Пользователь найден?"}
C -->|нет| F["Ошибка входа"]
C -->|да| D["Хэширование введенного пароля с солью"]
D --> E{"Хэш совпал?"}
E -->|нет| F
E -->|да| G["Запись успешного входа"]
G --> H["Открытие главного окна"]
F --> I["Запись неуспешной попытки"]
I --> J["CAPTCHA или блокировка"]
Схема регистрации¶
Регистрация создает новую запись в Users. До сохранения проверяются логин, пароль, подтверждение пароля и ФИО.
Главное правило: пароль хэшируется до записи в базу данных.
Если два пользователя выберут пароль 123456, в базе будут разные хэши, потому что для каждого пользователя создается своя соль.
using System.Security.Cryptography;
using System.Text;
public static class PasswordHasher
{
public static string GenerateSalt()
{
return Convert.ToBase64String(RandomNumberGenerator.GetBytes(16));
}
public static string HashPassword(string password, string salt)
{
using var sha256 = SHA256.Create();
byte[] bytes = Encoding.UTF8.GetBytes(password + salt);
return Convert.ToBase64String(sha256.ComputeHash(bytes));
}
public static bool Verify(string password, string salt, string expectedHash)
{
return HashPassword(password, salt) == expectedHash;
}
}
CAPTCHA¶
CAPTCHA в учебном приложении может быть текстовой. Смысл не в сложной графике, а в дополнительной проверке перед входом.
При открытии формы генерируется код. Пользователь вводит его в отдельное поле, а приложение сравнивает введенный текст с текущим значением. После ошибки код меняется, чтобы нельзя было повторять одну и ту же попытку.
public class CaptchaService
{
private readonly Random _random = new Random();
private const string Alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
public string CurrentCode { get; private set; } = "";
public string Generate()
{
CurrentCode = new string(
Enumerable.Range(0, 5)
.Select(_ => Alphabet[_random.Next(Alphabet.Length)])
.ToArray());
return CurrentCode;
}
public bool Validate(string input)
{
return string.Equals(CurrentCode, input?.Trim(), StringComparison.OrdinalIgnoreCase);
}
}
Где выполнять проверки¶
Часть проверок выполняется на форме, часть — в сервисах и базе данных.
| Проверка | Где выполнять |
|---|---|
| поле логина пустое | форма входа или регистрации |
| логин уже существует | UserRepository и уникальный индекс в БД |
| пароль слишком короткий | форма регистрации |
| пароль неверный | AuthService |
| CAPTCHA неверная | форма входа через CaptchaService |
| пользователь отключен | AuthService |
Такое разделение делает код понятнее: форма отвечает за ввод, сервис — за сценарий, репозиторий — за данные.
Блокировка¶
Блокировка защищает от многократного ввода пароля. В учебном варианте можно хранить счетчик ошибок в AuthService, а в расширенном варианте — записывать блокировку в таблицу пользователя.
Минимальное правило: 3 ошибки подряд блокируют вход на 30 секунд. После успешного входа счетчик ошибок сбрасывается.
Состояния входа¶
Форма входа работает не только в состоянии «ожидание ввода». У нее есть несколько состояний:
| Состояние | Что происходит |
|---|---|
| обычный ввод | пользователь вводит логин, пароль и CAPTCHA |
| ошибка CAPTCHA | CAPTCHA обновляется, пароль можно оставить или очистить |
| ошибка пароля | счетчик ошибок увеличивается |
| блокировка | кнопка входа временно недоступна или вход отклоняется |
| успешный вход | открывается главное окно |
Если состояния не продумать, часто появляются ошибки: CAPTCHA не обновляется, счетчик не сбрасывается, главное окно открывается после неверного кода.
Хранение текущего пользователя¶
После входа приложение должно знать, кто работает в системе. Обычно объект User передается в главное окно:
User user = _authService.Login(login, password);
var mainForm = new MainForm(user);
mainForm.Show();
Главное окно использует роль пользователя, чтобы настроить меню:
Для Windows Forms обработчик входа может выглядеть так:
private void btnLogin_Click(object sender, EventArgs e)
{
if (!_captcha.Validate(txtCaptcha.Text))
{
lblCaptcha.Text = _captcha.Generate();
MessageBox.Show("CAPTCHA введена неверно.");
return;
}
User user = _authService.Login(txtLogin.Text.Trim(), txtPassword.Text);
var mainForm = new MainForm(user);
mainForm.Show();
Hide();
}
В WPF та же логика остается в коде окна, меняются только имена элементов:
private void LoginButton_Click(object sender, RoutedEventArgs e)
{
if (!_captcha.Validate(CaptchaTextBox.Text))
{
CaptchaTextBlock.Text = _captcha.Generate();
MessageBox.Show("CAPTCHA введена неверно.");
return;
}
User user = _authService.Login(LoginTextBox.Text.Trim(), PasswordBox.Password);
var window = new MainWindow(user);
window.Show();
Close();
}
Разделение ответственности¶
| Класс | За что отвечает |
|---|---|
PasswordHasher |
соль, хэш, проверка пароля |
CaptchaService |
генерация и проверка CAPTCHA |
UserRepository |
чтение и запись пользователей |
AuthService |
сценарий входа, ошибки, блокировка |
LoginForm/LoginWindow |
ввод данных и сообщения пользователю |
Частые ошибки реализации¶
- создавать нового
AuthServiceпри каждом нажатии кнопки входа, из-за чего счетчик ошибок сбрасывается; - проверять пароль до CAPTCHA, хотя пользователь уже ввел неверный код;
- хранить пароль в таблице
Usersв открытом виде; - не записывать неудачные попытки входа;
- открывать главное окно через
Show, но не скрывать форму входа; - не передавать текущего пользователя в главное окно.