= Други теми = == Безбедност == === Спречување на 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 за реални мерења во реално време. === Сценарио 1: Тековен залиха по складиште === Цел: Прикажува вкупниот број производи и вредност на залихата по складишта. {{{ SELECT w.warehouse_id, w.name AS warehouse_name, SUM(ws.quantity_on_hand) AS total_units, SUM(ws.quantity_on_hand * p.unit_price) AS total_stock_value FROM warehouse_stock ws JOIN warehouse w ON ws.warehouse_id = w.warehouse_id JOIN product p ON ws.product_id = p.product_id GROUP BY w.warehouse_id, w.name ORDER BY total_stock_value DESC; }}} ==== 1.1. Без индекс ==== Кога се поврзува табелата **warehouse_stock** со табелата **product** преку колоната **product_id** (JOIN), ако нема индекс, базата на податоци ќе ги скенира сите редови. {{{ "HashAggregate (cost=705.70..706.95 rows=100 width=262) (actual time=102.460..102.484 rows=3.00 loops=1)" " Group Key: w.warehouse_id" " Batches: 1 Memory Usage: 32kB" " Buffers: shared hit=164 dirtied=1" " -> Hash Join (cost=191.75..518.20 rows=15000 width=232) (actual time=4.965..78.081 rows=15000.00 loops=1)" " Hash Cond: (ws.product_id = p.product_id)" " Buffers: shared hit=164 dirtied=1" " -> Hash Join (cost=12.25..299.29 rows=15000 width=230) (actual time=0.282..37.808 rows=15000.00 loops=1)" " Hash Cond: (ws.warehouse_id = w.warehouse_id)" " Buffers: shared hit=97 dirtied=1" " -> Seq Scan on warehouse_stock ws (cost=0.00..246.00 rows=15000 width=12) (actual time=0.103..20.064 rows=15000.00 loops=1)" " Buffers: shared hit=96" " -> Hash (cost=11.00..11.00 rows=100 width=222) (actual time=0.068..0.069 rows=3.00 loops=1)" " Buckets: 1024 Batches: 1 Memory Usage: 9kB" " Buffers: shared hit=1 dirtied=1" " -> Seq Scan on warehouse w (cost=0.00..11.00 rows=100 width=222) (actual time=0.029..0.032 rows=3.00 loops=1)" " Buffers: shared hit=1 dirtied=1" " -> Hash (cost=117.00..117.00 rows=5000 width=10) (actual time=4.433..4.444 rows=5000.00 loops=1)" " Buckets: 8192 Batches: 1 Memory Usage: 279kB" " Buffers: shared hit=67" " -> Seq Scan on product p (cost=0.00..117.00 rows=5000 width=10) (actual time=0.044..1.930 rows=5000.00 loops=1)" " Buffers: shared hit=67" "Planning:" " Buffers: shared hit=86" "Planning Time: 26.724 ms" "Execution Time: 103.080 ms" }}} '''Метод''': Parallel Seq Scan (секвенцијално пребарување). Базата на податоци мора да ги прочита сите 500.000 редови еден по еден. '''Времетраење: 173.154 ms''' '''Анализа:''' Многу бавно. [[Image(without_index_1.png, 800px)]] ==== 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. '''Времетраење: 94.976 ms ''' '''Анализа:''' Многу по брзо. [[Image(with_index_1.png, 800px)]] === Сценарио 2: Пребарување на производи === Цел: Корисникот да може да пребарува производи според името (на пример: производи што започнуваат со "Laptop"). {{{ SELECT * FROM product WHERE name LIKE 'Laptop%'; EXPLAIN ANALYZE SELECT * FROM product WHERE name LIKE 'Laptop%'; }}} ==== 2.1. Без индекс ==== '''Метод''': Seq Scan. Сите 100.000 редови со производи се проверени еден по еден со споредба на текст. '''Времетраење: 142.270 ms''' '''Анализа:''' Многу бавно. [[Image(without_index_2.png, 800px)]] ==== 2.2. Стратегија за индексирање (Partial Index) ==== За да се забрзаат текстуалните пребарувања, е применет B-Tree индекс на колоната '''name'''. Параметарот text_pattern_ops овозможува LIKE барањата да можат да го користат индексот. '''Применет индекс:''' {{{ CREATE INDEX idx_product_name ON product(name text_pattern_ops); }}} ==== 2.3. Со индекс ==== '''Метод''': Index Scan. '''Времетраење: 0.470 ms ''' '''Анализа:''' Многу по брзо. [[Image(with_index_2.png, 800px)]]