= Други теми = == Безбедност == === Спречување на 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), ако нема индекс, базата на податоци ќе ги скенира сите редови. {{{ EXPLAIN ANALYZE SELECT w.name, SUM(ws.quantity_on_hand), SUM(ws.quantity_on_hand * p.unit_price) FROM stock_management.warehouse_stock ws JOIN stock_management.warehouse w ON ws.warehouse_id = w.warehouse_id JOIN stock_management.product p ON ws.product_id = p.product_id GROUP BY w.warehouse_id, w.name; }}} {{{ "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" }}} '''Времетраење: 103.080 ms''' '''Анализа:''' Многу бавно. ==== 1.2. Со индекс ==== За пребарувањето по product_id во табелата warehouse_stock да биде побрзо '''Применет индекс:''' {{{ CREATE INDEX idx_wh_stock_product_id ON stock_management.warehouse_stock(product_id); }}} {{{ "HashAggregate (cost=705.70..706.95 rows=100 width=262) (actual time=29.541..29.548 rows=3.00 loops=1)" " Group Key: w.warehouse_id" " Batches: 1 Memory Usage: 32kB" " Buffers: shared hit=164" " -> Hash Join (cost=191.75..518.20 rows=15000 width=232) (actual time=2.793..18.549 rows=15000.00 loops=1)" " Hash Cond: (ws.product_id = p.product_id)" " Buffers: shared hit=164" " -> Hash Join (cost=12.25..299.29 rows=15000 width=230) (actual time=0.060..9.059 rows=15000.00 loops=1)" " Hash Cond: (ws.warehouse_id = w.warehouse_id)" " Buffers: shared hit=97" " -> Seq Scan on warehouse_stock ws (cost=0.00..246.00 rows=15000 width=12) (actual time=0.025..1.711 rows=15000.00 loops=1)" " Buffers: shared hit=96" " -> Hash (cost=11.00..11.00 rows=100 width=222) (actual time=0.019..0.021 rows=3.00 loops=1)" " Buckets: 1024 Batches: 1 Memory Usage: 9kB" " Buffers: shared hit=1" " -> Seq Scan on warehouse w (cost=0.00..11.00 rows=100 width=222) (actual time=0.011..0.013 rows=3.00 loops=1)" " Buffers: shared hit=1" " -> Hash (cost=117.00..117.00 rows=5000 width=10) (actual time=2.710..2.710 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.010..1.133 rows=5000.00 loops=1)" " Buffers: shared hit=67" "Planning:" " Buffers: shared hit=12" "Planning Time: 0.761 ms" "Execution Time: 29.767 ms" }}} '''Времетраење: 29.767 ms ''' '''Анализа:''' Многу по брзо.