= Други теми = == Безбедност == === Спречување на SQL Injection === Користиме Entity Framework Core (ORM). EF Core автоматски ги параметризира сите LINQ прашања (queries). Ова спречува напаѓачите да вметнат злонамерни SQL команди преку полињата за внес. * EF Core го третира **username** како параметар (@p0), а не како извршлив код. * Ова спречува SQL Injection напади (на пр., ' OR 1=1 --). {{{ public async Task AuthenticateAsync(string username, string password) { var user = await _context.Users .FirstOrDefaultAsync(u => u.Username == username && u.IsActive); if (user == null) return null; bool isHashed = user.Password.StartsWith("$2") && user.Password.Length == 60; if (isHashed) { if (BCrypt.Net.BCrypt.Verify(password, user.Password)) return user; } else { if (user.Password == password) { user.Password = BCrypt.Net.BCrypt.HashPassword(password); await _context.SaveChangesAsync(); return user; } } return null; } }}} === Хеширање на лозинки (Заштита на податоци) === Лозинките се зачувуваат како хеш вредности со користење на алгоритмот BCrypt, а не како обичен текст. {{{ public async Task CreateUserAsync(User user, string password) { using var transaction = await _context.Database.BeginTransactionAsync(); try { user.Password = BCrypt.Net.BCrypt.HashPassword(password); _context.Users.Add(user); await _context.SaveChangesAsync(); await transaction.CommitAsync(); return true; } catch { await transaction.RollbackAsync(); return false; } } }}} === Безбедност на Database Context (Row-Level идентификација) === Го пренесуваме идентитетот на моментално најавениот корисник од Application Layer до Database Layer (PostgreSQL) користејќи Session Variables. Ова и овозможува на базата на податоци да знае кој ја извршува операцијата. {{{ public override async Task SaveChangesAsync(CancellationToken cancellationToken = default) { var username = _httpContextAccessor.HttpContext?.User?.Identity?.Name ?? "system"; await Database.ExecuteSqlRawAsync("SELECT set_config('app.current_user', {0}, false)", new[] { username }, cancellationToken); return await base.SaveChangesAsync(cancellationToken); } }}} === Авторизација (Role-Based Access Control) === Го ограничуваме пристапот до Controllers и Actions со користење на атрибутот ** [Authorize] **. Само автентицирани корисници со валидни cookies можат да пристапат до овие ресурси. * Овој атрибут осигурува дека само најавени корисници можат да пристапат до било која акција во овој контролер. * Неавтентицираните барања се пренасочуваат кон страницата за најава (Login page). {{{ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace StockMaster.Controllers { [Authorize] public class ReportController : Controller { private readonly IReportService _reportService; public ReportController(IReportService reportService) { _reportService = reportService; } public IActionResult Index() { return View(); } // ... други акции } } }}} === Безбедност на база на податоци базирана на логика (Triggers) === Користиме Database Triggers за да спроведеме безбедносни правила што не можат да бидат заобиколени од апликацијата. Поточно, спречуваме корисник да ја избрише сопствената account за да обезбедиме стабилност на системот и следење на активности. {{{ CREATE OR REPLACE FUNCTION stock_management.prevent_self_delete() RETURNS TRIGGER AS $$ BEGIN IF OLD.username = current_setting('app.current_user', true) THEN RAISE EXCEPTION 'You cannot delete your own account.'; END IF; RETURN OLD; END; $$ LANGUAGE plpgsql; CREATE OR REPLACE TRIGGER trg_prevent_self_delete BEFORE DELETE ON stock_management.users FOR EACH ROW EXECUTE FUNCTION stock_management.prevent_self_delete(); }}} == Пеформанси - Индекси == За да се зголеми перформансата на базата на податоци, се применуваат стратегии за индексирање и се анализира споредбата на перформансите во состојби со индекс и без индекс. Тестовите се извршени со користење на командата EXPLAIN ANALYZE за реални мерења во реално време. Обем на тест податоци: 250.000+ редови (Табела за продажби), 50.000+ редови (Табела за нарачки), 10.000+ производи (Табела за производи) {{{ SET search_path TO stock_management; TRUNCATE sale, purchase_order, product RESTART IDENTITY CASCADE; -- 10.000 производи INSERT INTO product (name, description, sku, unit_price, category_id, supplier_id) SELECT 'Product ' || i, 'Desc', 'SKU-' || i, (random() * 100)::numeric(12,2), 1, 1 FROM generate_series(1, 10000) AS i; -- 250.000 продажби INSERT INTO sale (date_time, total_amount, user_id, customer_id, warehouse_id) SELECT NOW() - (random() * interval '365 days'), (random() * 5000)::numeric(15,2), 1, 1, 1 FROM generate_series(1, 250000) AS i; -- 50.000 нарачки INSERT INTO purchase_order (order_date, expected_delivery_date, status, supplier_id, warehouse_id) SELECT CURRENT_DATE, CURRENT_DATE + i, CASE WHEN i % 2 = 0 THEN 'Pending' ELSE 'Received' END, 1, 1 FROM generate_series(1, 50000) AS i; }}} === Сценарио 1: Листaње на нарачки во очекување === Цел: Персоналот во магацинот да ги гледа само нарачките чиј статус е *Pending* (Во очекување). Завршените нарачки *Received* имаат архивски карактер и создаваат непотребно оптоварување во оперативните пребарувања. {{{ SELECT * FROM purchase_order WHERE status = 'Pending'; }}} ==== 1.1. Без индекс (Before Optimization) ==== Во базата на податоци нема дефинирано индекс на полето за датум. '''Метод''': Parallel Seq Scan (секвенцијално пребарување). Базата на податоци мора да ги прочита сите 50.000 редови еден по еден. '''Времетраење: 45.2 ms''' '''Анализа:''' Многу бавно. ==== 1.2. Стратегија за индексирање (Partial Index) ==== Наместо да се индексира целата табела, е креиран делумен индекс (Partial Index) кој ги вклучува само записите каде што status = 'Pending'. Ова го намалува големината на индексот и ја забрзува извршувањето на барањето. '''Применет индекс:''' {{{ CREATE INDEX idx_po_status_pending ON purchase_order(expected_delivery_date) WHERE status = 'Pending'; }}} ==== 1.3. Со индекс ==== Во базата на податоци нема дефинирано индекс на полето за датум. '''Метод''': Index Scan. '''Времетраење: 0.9 ms ''' '''Анализа:''' Многу по брзо.