Онлайн час в Google Meet
Лого на 91. НЕГ „Проф. Константин Гълъбов“

12. клас · Модул 4 · Урок 20

Още функционалност в ИС УЧИСЕ

Удостоверяване с потребителско име и парола, регистриране на сесиите и журнал на изпълнените заявки — трите неща, които превръщат програмата в информационна система.

Какво липсва още

Системата вече чете и редактира данни. Но една ИС, в чиято експлоатация участват много хора, има нужда от още три неща: да знае кой работи с нея, да пази кога е работил и да помни какво е променил. С тях се занимава този урок.

Служебните таблици

В базата School създаваме две служебни таблици. Префиксът is в имената им подсказва, че те са за нуждите на информационната система, а не съдържат данни за училището.

CREATE TABLE [dbo].[isUsers] (
    [idUser]   int IDENTITY PRIMARY KEY,
    [username] varchar(50),
    [password] varchar(50)
);

CREATE TABLE [dbo].[isSessions] (
    [idSesion] int IDENTITY PRIMARY KEY,
    [username] varchar(50),
    [start]    datetime
);
  • isUsers пази потребителското име и паролата на всеки, когото сме създали на сървъра.
  • isSessions регистрира всяко успешно удостоверяване — така знаем кой по кое време е влизал.
  • Ключовете са IDENTITY, тоест се увеличават автоматично с 1 при добавяне на нов ред.

В isUsers първо въвеждаме суперпотребител, нужен за управляване на самия процес по удостоверяване — например admin с парола admpass. След него влизат имената и паролите на потребителите, които сме регистрирали на сървъра.

Удостоверяването

Формата за удостоверяване има кутия за потребител, кутия за парола и бутон ОК. Ето какво стои зад бутона:

private void button1_Click(object sender, EventArgs e)
{   string connString = @"Server=localhost;Database=School;" +
                        "User Id=admin;Password=admpass;";
    string queryString = "SELECT password FROM isUsers WHERE " +
                         "username='" + userBox.Text + "';";
    //MessageBox.Show(queryString);
    using (SqlConnection connection = new SqlConnection(connString))
    {   SqlCommand command = new SqlCommand(queryString, connection);
        connection.Open();
        using (SqlDataReader reader = command.ExecuteReader())
        {   if (reader.HasRows == true)
            {   reader.Read();
                Form1.dbPassword = reader[0].ToString();
                if(Form1.dbPassword != passwordBox.Text)
                {   MessageBox.Show("Няма такъв потребител или парола"); }
                else
                {   Form1.dbUser = userBox.Text;
                    this.Close();
                }
            }
            else { MessageBox.Show("Няма такъв потребител или парола"); }
        }
    }
}
  • Тук за пръв път свързващият низ е стандартен — с потребителско име и парола. Ползваме суперпотребителя, защото само той има достъп до служебните таблици. Тези данни не се дават на реален потребител.
  • Свойството HasRows на четеца е true само когато заявката е извлякла данни. Без тази проверка Read() хвърля изключение и програмата спира аварийно — точно случаят от предишния практикум.
  • Отказът има две места: когато няма такъв потребител (празен резултат) и когато паролата не съвпада. Затова и съобщението се среща два пъти.
  • При успех името и паролата остават в Form1.dbUser и Form1.dbPassword — ще потрябват веднага.

Регистриране на сесията

Веднага след удостоверяването добавяме ред в isSessions — преди да затворим формата с this.Close():

queryString = "INSERT INTO isSessions VALUES ('" + Form1.dbUser +
        "', '" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + "');";
// MessageBox.Show(queryString);
connection.Close();
command = new SqlCommand(queryString, connection);
connection.Open();
command.ExecuteNonQuery();
  • Моментът се взема от свойството Now на класа DateTime. За да влезе в SQL оператор, се форматира с ToString() — приемливият за MS SQL Server формат е "yyyy-MM-dd HH:mm:ss".
  • За пръв път при едно събитие изпълняваме две заявки. Затова първата връзка се затваря с Close(), създава се нова команда и връзката се отваря отново.

Стъпката, без която всичко е излишно

Сега идва най-важното. Навсякъде в програмата, където изпълняваме заявки с поверителен свързващ низ, той трябва да се замени със стандартен — с данните на удостоверения потребител:

@"Server=localhost;Database=School;User Id='" + Form1.dbUser +
  "';Password='" + Form1.dbPassword + "';";

Оттук нататък можем да ограничим достъпа на различните потребители — с Enabled = False на бутоните, стартиращи съответните функции, а за разрешеното — със съответните роли в сървъра.

Журнал на системата

Архивирането на базата не стига. Между последното съхраняване и евентуалния срив потребителите може да са направили много промени — редактирали са данни, добавили са редове, изтрили са ненужни. След възстановяване от архива тези промени ги няма.

Единственият начин да се поправят щетите е журнал на системата, в който се вписват всички изпълнени след архивирането заявки. Тогава след възстановяването те могат да се повторят и базата да се върне напълно.

CREATE TABLE isJournal (
    idQuery  int IDENTITY PRIMARY KEY,
    query    varchar(50),
    executed datetime
);
queryString = "INSERT INTO isJournal VALUES ('" + queryQyery +
        "', '" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + "');";
connection.Close();
command = new SqlCommand(queryString, connection);
connection.Open();
command.ExecuteNonQuery();

Кодът се добавя навсякъде, където предстои да се изпрати SQL оператор за изпълнение. И тук операторът е втори за същото събитие — не пропускай да затвориш първата връзка и да я отвориш отново.

Как се пазят пароли наистина

Схемата в урока работи, но има три слабости, които в реална система са недопустими. Добре е да ги знаеш:

СлабостКакво се прави вместо това
Паролите се пазят в чист вид в isUsersпази се само хеш на паролата (SHA-256 с „сол“, а по-добре bcrypt) — от него оригиналът не се възстановява
Паролата се изтегля в програмата и се сравнява тамизпраща се хешът на въведеното и сравнението става в базата — паролата никога не пътува обратно
username се долепя към текста на заявкатапараметризирана заявка, както в урок 17

Контрол и оценка

Какво трябва да запомниш

  • Служебните таблици (isUsers, isSessions, isJournal) обслужват системата, не предметната област.
  • HasRows се проверява преди Read() — иначе празният резултат чупи програмата.
  • Датата за SQL оператор се форматира с "yyyy-MM-dd HH:mm:ss".
  • Две заявки при едно събитие искат затваряне и повторно отваряне на връзката.
  • След удостоверяването свързващият низ се сменя със стандартен — чак тогава правата на потребителя в сървъра започват да важат.
  • Журналът пази изпълнените заявки, за да може базата да се възстанови до състоянието преди срива.