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

Модул 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. След него влизат имената и паролите на потребителите, които сме регистрирали на сървъра.

Внимание

При създаването на потребителските профили в MS SQL Server НЕ слагай отметка User must change password at next login. Ако я сложиш, потребителят ще трябва да смени паролата при първото свързване и функционалността няма да работи. Профилът на суперпотребителя трябва да е регистриран и в Security/Users на базата.

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

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

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 + "';";

Ключово

Без тази стъпка всички направени усилия са безсмислени: програмата щеше да продължи да работи с правата на Windows профила, а не с тези на влезлия потребител. Едва сега ролите и правата, които сме определили в сървъра, започват да важат.

Оттук нататък можем да ограничим достъпа на различните потребители — с 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 оператор за изпълнение. И тук операторът е втори за същото събитие — не пропускай да затвориш първата връзка и да я отвориш отново.

Бележка

Обърни внимание на размера: varchar(50) побира 50 знака, а заявките от предишните уроци са в пъти по-дълги. За истински журнал колоната трябва да е поне varchar(MAX).

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

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

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

Внимание

Ако някой се добере до isUsers, при чист текст той има паролите на всички потребители — включително за други системи, където хората често ползват същата парола. При хеширане не получава нищо използваемо.

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

Задачи 1 и 2

Добави в информационната система удостоверяване с потребителско име и парола, както и регистриране на започването на сесии. Разшири функционалността с информация за края на сесията: добави атрибут в isSessions, запази момента на удостоверяване в Form1.sessionSart и постави кода при събитието Close на бутона Край в менюто Начало.

Задачи 3 и 4

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

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

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