Перейти к содержанию

Этап 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. До сохранения проверяются логин, пароль, подтверждение пароля и ФИО.

Главное правило: пароль хэшируется до записи в базу данных.

password -> salt -> hash -> INSERT INTO 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();

Главное окно использует роль пользователя, чтобы настроить меню:

usersMenuItem.Visible = currentUser.RoleName == "admin";

Для 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, но не скрывать форму входа;
  • не передавать текущего пользователя в главное окно.