| Version 10 (modified by , 3 days ago) ( diff ) |
|---|
Други теми (Перформанси, безбедност)
Безбедност
- Апликацијата користи Spring Security што е докажан стандард за сетирање на безбедност во апликациите.
- Се користи дозволување на клиент кој може да праќа барања до backend
config.setAllowedOrigins(List.of("http://localhost:5173")); - Само рутата за логирање на корисници е дозволена за целата јавност, другите рути бараат автентикација
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/login").permitAll()
.anyRequest().authenticated()
)
- Сите корисници се креирани од директор админ и нема стандардна регистрација.
Хеширање на лозинки (Заштита на податоци)
Апликацијата користи BCryptPasswordEncoder како начин за хеширање на лозинки.
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
Кој потоа се користи при хеширање:
String encodedPass = passwordEncoder.encode( blagajnikRequestDTO.getLozinka());
Авторизација (Role-Based Access Control)
Апликацијата користи Roles за дозвола на пристап на некоја активност. Исто така се користи и method security т.е. се користи @PreAuthorize анотацијата за заштита на рутите:
@PreAuthorize("hasRole('DIREKTOR_ADMIN')")
@GetMapping
public ResponseEntity<List<KlasResponseDTO>> getAllKlasovi(){
List<KlasResponseDTO> klasovi = this.klasService.getAllKlasovi();
return new ResponseEntity<>(klasovi, HttpStatus.OK);
}
@GetMapping("/{classId}/students")
@PreAuthorize("hasAnyRole('KLASEN_RAKOVODITEL','DIREKTOR_ADMIN', 'PREDMETEN_NASTAVNIK', 'RODITEL')")
public ResponseEntity<List<UcenikResponseDTO>> getAllStudentsWhoAreInTheSameClass(@PathVariable UUID classId){
List<UcenikResponseDTO> getAllStudentsWhoAreInTheSameClass = this.klasService.getAllStudentsWhoAreInTheSameClass(classId);
return new ResponseEntity<>(getAllStudentsWhoAreInTheSameClass, HttpStatus.OK);
}
Користење на сесии
Апликацијата користи сесии и сесиско колаче за автентикација на сесијата на корисникот.
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
);
Но, и автоматско бришење на сесиското колаче при одјава:
.logout(logout->logout
.logoutUrl("/api/auth/logout")
.deleteCookies("JSESSIONID")
.logoutSuccessHandler((request, response, authentication) -> {
response.setStatus(200);
})
Спречување на SQL Injection
За извршување на SQL прашањата, користиме binding. Односно Spring Boot ги третира овие параметри како податоци а не како извршен код. Дел од кодот:
@Modifying
@Transactional
@Query(nativeQuery = true, value = "INSERT INTO Usna_Ocenka (id, tema, osvoeni_poeni, max_poeni, vid_isprasuvanje) VALUES( :id,:tema, :osvoeni_poeni, :max_poeni, :vid_isprasuvanje)")
void insertUsnaOcenka(
@Param("id") UUID id,
@Param("tema") String tema,
@Param("osvoeni_poeni") Integer osvoeniPoeni,
@Param("max_poeni") Integer maxPoeni,
@Param("vid_isprasuvanje") String vidUsnoIsprasuvanje
);
Безбеднст на ниво на база
Имаме имплементирано посебен корисник кој што не е главниот user:
Исто така, користиме тип enum на ниво на база дополнително да се осигураме дека станува збор за вистинската категорија.
CREATE TYPE payment_status AS ENUM (
'KREIRANO',
'TX_SUBMITTED',
'ODOBRENO',
'ZAVRSENO',
'FAILED'
);
CREATE TYPE notice_status AS ENUM (
'PENDING',
'APPROVED',
'REJECTED'
);
CREATE TABLE Plakjanje (
id UUID PRIMARY KEY DEFAULT gen_random_uuid (),
status payment_status NOT NULL,
tx_hash VARCHAR(100) UNIQUE,
valuta VARCHAR(10) NOT NULL,
plateno_Na TIMESTAMP,
iznos DECIMAL(38, 18) NOT NULL,
platenoOdRoditel_Id UUID NOT NULL,
soopstenie_za_plakjanje_id UUID NOT NULL,
created_at TIMESTAMP DEFAULT now(),
UNIQUE (
platenoOdRoditel_Id,
soopstenie_za_plakjanje_id
),
CONSTRAINT chk_tx_hash_when_paid CHECK (
(
status = 'KREIRANO'
AND tx_hash IS NULL
)
OR (
status IN (
'TX_SUBMITTED',
'ODOBRENO',
'ZAVRSENO',
'FAILED'
)
AND tx_hash IS NOT NULL
)
),
FOREIGN KEY (soopstenie_za_plakjanje_id) REFERENCES SoopstenieZa_Plakjanje (id),
FOREIGN KEY (platenoOdRoditel_Id) REFERENCES Roditel (id)
);
Користиме и pgcrypto модулот за генерирање на UUID, што дополнително ја зголемува безбедноста за погодување на id и влечење на информации.
CREATE EXTENSION IF NOT EXISTS "pgcrypto"; id UUID PRIMARY KEY DEFAULT gen_random_uuid (),
Перформанси
Сценарио 1 - Преглед на присуство за ученик - полугодишно
Во распоредот за часови, може да имаме 7 часа дневно, 5 дена во неделата. Тоа се 35 часа неделно или 140 часа месечно. Во превод, за еден месец имаме 140 записи за присуство на месечно ниво.
Присуството може да се филтрира по месеци поради тоа што за едно полугодие имаме 560 записи за присуство и тоа само за еден ученик. Но, доколку класниот раководител или родителот сака да го види присуството за едно полугодие за ученикот, тогаш мора да се пребара низ огромна табела.
SQL Query за прво полугодие за околу 30 ученици:
explain ANALYZE SELECT p.id AS p_id, p.datum, p.status, p.zabeleska, k.id AS n_id, k.e_posta, k.ime, k.prezime, k.pol, k.adresa, k.kreiranod_id, k.active, c.id AS c_id, c.ime, c.reden_cas, c.predmet_id, c.active, c.denvo_nedelata FROM prisustvo p JOIN nastavnik n ON p.evidentiranoOd_Id = n.id JOIN korisnik k ON n.id = k.id JOIN cas c ON p.zaCasot_Id = c.id WHERE p.seOdnesuvaNaUcenikot_Id ='18d82d24-9d9f-4c0b-9692-f7a87174cb4c' AND p.datum BETWEEN '2026-09-01' AND '2026-12-31' ORDER BY datum DESC
Анализирање:
Sort (cost=699.80..701.65 rows=737 width=863) (actual time=3.782..3.831 rows=736 loops=1)
Sort Key: p.datum DESC
Sort Method: quicksort Memory: 167kB
-> Nested Loop (cost=11.67..664.70 rows=737 width=863) (actual time=0.107..3.410 rows=736 loops=1)
-> Hash Join (cost=11.51..637.59 rows=737 width=726) (actual time=0.094..2.957 rows=736 loops=1)
Hash Cond: (p.evidentiranood_id = k.id)
-> Nested Loop (cost=0.16..624.17 rows=737 width=95) (actual time=0.059..2.692 rows=736 loops=1)
-> Seq Scan on prisustvo p (cost=0.00..601.99 rows=737 width=79) (actual time=0.033..2.300 rows=736 loops=1)
Filter: ((datum >= '2026-09-01'::date) AND (datum <= '2026-12-31'::date) AND (seodnesuvanaucenikot_id = '18d82d24-9d9f-4c0b-9692-f7a87174cb4c'::uuid))
Rows Removed by Filter: 17435
-> Memoize (cost=0.16..0.25 rows=1 width=16) (actual time=0.000..0.000 rows=1 loops=736)
Cache Key: p.evidentiranood_id
Cache Mode: logical
Hits: 721 Misses: 15 Evictions: 0 Overflows: 0 Memory Usage: 2kB
-> Index Only Scan using nastavnik_pkey on nastavnik n (cost=0.15..0.24 rows=1 width=16) (actual time=0.002..0.002 rows=1 loops=15)
Index Cond: (id = p.evidentiranood_id)
Heap Fetches: 15
-> Hash (cost=10.60..10.60 rows=60 width=663) (actual time=0.026..0.027 rows=26 loops=1)
Buckets: 1024 Batches: 1 Memory Usage: 11kB
-> Seq Scan on korisnik k (cost=0.00..10.60 rows=60 width=663) (actual time=0.010..0.016 rows=26 loops=1)
-> Memoize (cost=0.16..0.24 rows=1 width=153) (actual time=0.000..0.000 rows=1 loops=736)
Cache Key: p.zacasot_id
Cache Mode: logical
Hits: 699 Misses: 37 Evictions: 0 Overflows: 0 Memory Usage: 7kB
-> Index Scan using cas_pkey on cas c (cost=0.15..0.23 rows=1 width=153) (actual time=0.001..0.001 rows=1 loops=37)
Index Cond: (id = p.zacasot_id)
Planning Time: 0.436 ms
Execution Time: 3.960 ms
Можеме да видиме дека
- Planning Time е: 0.436 ms
- Execution Time: 3.960 ms
- Начин на скенирање: Seq Scan
- Се користи quick sort алгоритам
- Се користи index за наставникот кој го внел присуството: evidentiranood_id
- Се користи index за часот за кој е присуството.
Предложени индекси
- Индекс за филтрирање на ученикот и датумот
CREATE INDEX idx_prisustvo_ucenik_datum ON prisustvo (seOdnesuvaNaUcenikot_Id, datum DESC);
- Индекси за наставникот, но и за часот
CREATE INDEX idx_prisustvo_evidentiranood ON prisustvo (evidentiranoOd_Id); CREATE INDEX idx_prisustvo_zacasot ON prisustvo (zaCasot_Id);
Анализа после додавање на индексите:
Sort (cost=357.31..359.15 rows=737 width=176) (actual time=1.400..1.452 rows=736 loops=1)
Sort Key: p.datum DESC
Sort Method: quicksort Memory: 167kB
-> Hash Join (cost=18.53..322.21 rows=737 width=176) (actual time=0.154..0.992 rows=736 loops=1)
Hash Cond: (p.zacasot_id = c.id)
-> Hash Join (cost=16.67..318.21 rows=737 width=138) (actual time=0.115..0.714 rows=736 loops=1)
Hash Cond: (p.evidentiranood_id = k.id)
-> Hash Join (cost=15.09..314.38 rows=737 width=95) (actual time=0.081..0.446 rows=736 loops=1)
Hash Cond: (p.evidentiranood_id = n.id)
-> Bitmap Heap Scan on prisustvo p (cost=13.68..310.58 rows=737 width=79) (actual time=0.058..0.187 rows=736 loops=1)
Recheck Cond: ((seodnesuvanaucenikot_id = '18d82d24-9d9f-4c0b-9692-f7a87174cb4c'::uuid) AND (datum >= '2026-09-01'::date) AND (datum <= '2026-12-31'::date))
Heap Blocks: exact=12
-> Bitmap Index Scan on idx_prisustvo_ucenik_datum (cost=0.00..13.50 rows=737 width=0) (actual time=0.044..0.045 rows=736 loops=1)
Index Cond: ((seodnesuvanaucenikot_id = '18d82d24-9d9f-4c0b-9692-f7a87174cb4c'::uuid) AND (datum >= '2026-09-01'::date) AND (datum <= '2026-12-31'::date))
-> Hash (cost=1.18..1.18 rows=18 width=16) (actual time=0.013..0.014 rows=18 loops=1)
Buckets: 1024 Batches: 1 Memory Usage: 9kB
-> Seq Scan on nastavnik n (cost=0.00..1.18 rows=18 width=16) (actual time=0.006..0.009 rows=18 loops=1)
-> Hash (cost=1.26..1.26 rows=26 width=75) (actual time=0.027..0.028 rows=26 loops=1)
Buckets: 1024 Batches: 1 Memory Usage: 11kB
-> Seq Scan on korisnik k (cost=0.00..1.26 rows=26 width=75) (actual time=0.010..0.017 rows=26 loops=1)
-> Hash (cost=1.38..1.38 rows=38 width=54) (actual time=0.032..0.032 rows=38 loops=1)
Buckets: 1024 Batches: 1 Memory Usage: 12kB
-> Seq Scan on cas c (cost=0.00..1.38 rows=38 width=54) (actual time=0.016..0.021 rows=38 loops=1)
Planning Time: 0.832 ms
Execution Time: 1.548 ms
Може да се забележи:
- Planning Time: 0.832 ms
- Execution Time: 1.548 ms
- Bitmap Index Scan, Bitmap Heap Scan
- Се користи индексот: idx_prisustvo_ucenik_datum
- Немаме rows removed
Заклучок Можеме да забележиме дека времето на извршување се намали за 2 милисекунди. Овој пример работи само на ~30 ученици. Но доколку имаме повеќе тогаш перформансите би биле подобри. Исто така немаме тргање на редици, при филтрирањето, туку се зимаат тие што се потребни.
