Какво липсва още
Системата вече чете и редактира данни. Но една ИС, в чиято експлоатация участват много хора, има нужда от още три неща: да знае кой работи с нея, да пази кога е работил и да помни какво е променил. С тях се занимава този урок.
Ключово
Удостоверяването не е самоцел. Смисълът му идва накрая — когато програмата започне да се свързва с базата с името на влезлия потребител и правата, които сървърът му е дал.
Служебните таблици
В базата 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". - Две заявки при едно събитие искат затваряне и повторно отваряне на връзката.
- След удостоверяването свързващият низ се сменя със стандартен — чак тогава правата на потребителя в сървъра започват да важат.
- Журналът пази изпълнените заявки, за да може базата да се възстанови до състоянието преди срива.