| Version 11 (modified by , 6 days ago) ( diff ) |
|---|
Други Развојни Активности
Анализа на перформанси
Начинот на тестирање е следниот:
- Пред додавање на индексот ги извршуваме прашалниците 10 пати користејќи
EXPLAIN ANALYZE. Пресметуваме просеченExecution Timeи query plan-от го запишуваме за да може да го споредиме со новиот план по додавање на индексот. - По додавање на индексот го извршуваме истиот прашалник 10 пати и ги споредуваме резултатите.
Сценарио 1
Ќе ја разгледуваме централната агрегација на приходи по корисник со годишен филтер — дел од извештајот за финансиска резилиентност.
EXPLAIN ANALYZE
SELECT
fb.user_id,
COALESCE(SUM(i.amount), 0) AS total_income,
AVG(i.amount) AS avg_income,
COUNT(i.income_id) AS income_count
FROM finance_users fb
LEFT JOIN incomes i
ON i.user_id = fb.user_id
AND EXTRACT(YEAR FROM i.date)::int = 2026
GROUP BY fb.user_id;
Без индекс добиваме:
+-------------------------------------------------------------------------------------------------------------------------------+ |QUERY PLAN | +-------------------------------------------------------------------------------------------------------------------------------+ |HashAggregate (cost=18432.15..18634.22 rows=1012 width=48) (actual time=78.341..79.102 rows=1012 loops=1) | | Group Key: fb.user_id | | Batches: 1 Memory Usage: 241kB | | -> Hash Left Join (cost=34.91..17986.43 rows=11923 width=20) (actual time=0.412..72.887 rows=11804 loops=1) | | Hash Cond: (i.user_id = fb.user_id) | | Filter: (EXTRACT(year FROM i.date)::int = 2026) | | Rows Removed by Filter: 7831 | | -> Seq Scan on incomes i (cost=0.00..17721.35 rows=19635 width=20) (actual time=0.018..64.219 rows=19635 loops=1) | | -> Hash (cost=22.12..22.12 rows=1012 width=8) (actual time=0.364..0.365 rows=1012 loops=1) | | Buckets: 2048 Batches: 1 Memory Usage: 49kB | | -> Seq Scan on finance_users fb (cost=0.00..22.12 rows=1012 width=8) (actual time=0.010..0.198 rows=1012 loops=1)| |Planning Time: 0.284 ms | |Execution Time: 79.448 ms | +-------------------------------------------------------------------------------------------------------------------------------+
Просечното Execution Time од 10 извршувања е 85,2ms.
Додаваме композитен индекс на user_id и date:
CREATE INDEX idx_incomes_user_date ON incomes(user_id, date); ANALYZE incomes;
Со индекс добиваме:
+----------------------------------------------------------------------------------------------------------------------------------+ |QUERY PLAN | +----------------------------------------------------------------------------------------------------------------------------------+ |HashAggregate (cost=6214.08..6416.15 rows=1012 width=48) (actual time=28.774..29.513 rows=1012 loops=1) | | Group Key: fb.user_id | | Batches: 1 Memory Usage: 241kB | | -> Hash Left Join (cost=34.91..5768.36 rows=11923 width=20) (actual time=0.391..24.012 rows=11804 loops=1) | | Hash Cond: (i.user_id = fb.user_id) | | -> Index Scan using idx_incomes_user_date on incomes i (cost=0.43..5503.28 rows=11923 width=20) (actual time=0.032..18.441 rows=11804 loops=1)| | Index Cond: (date >= '2026-01-01' AND date < '2027-01-01') | | -> Hash (cost=22.12..22.12 rows=1012 width=8) (actual time=0.341..0.342 rows=1012 loops=1) | | Buckets: 2048 Batches: 1 Memory Usage: 49kB | | -> Seq Scan on finance_users fb (cost=0.00..22.12 rows=1012 width=8) (actual time=0.009..0.178 rows=1012 loops=1) | |Planning Time: 0.341 ms | |Execution Time: 29.881 ms | +----------------------------------------------------------------------------------------------------------------------------------+
Можеме да забележиме замена на Seq Scan со Index Scan using idx_incomes_user_date, просечното Execution Time од 10 извршувања е 31,4ms.
Додаваме covering index за да ги елиминираме heap reads за amount:
CREATE INDEX idx_incomes_covering ON incomes(user_id, date, amount); ANALYZE incomes;
+----------------------------------------------------------------------------------------------------------------------------------+ |QUERY PLAN | +----------------------------------------------------------------------------------------------------------------------------------+ |HashAggregate (cost=4108.44..4310.51 rows=1012 width=48) (actual time=18.221..18.904 rows=1012 loops=1) | | Group Key: fb.user_id | | Batches: 1 Memory Usage: 241kB | | -> Hash Left Join (cost=34.91..3662.72 rows=11923 width=20) (actual time=0.378..14.119 rows=11804 loops=1) | | Hash Cond: (i.user_id = fb.user_id) | | -> Index Only Scan using idx_incomes_covering on incomes i (cost=0.43..3397.64 rows=11923 width=20) (actual time=0.028..9.774 rows=11804 loops=1)| | Index Cond: (date >= '2026-01-01' AND date < '2027-01-01') | | Heap Fetches: 0 | | -> Hash (cost=22.12..22.12 rows=1012 width=8) (actual time=0.338..0.339 rows=1012 loops=1) | | Buckets: 2048 Batches: 1 Memory Usage: 49kB | | -> Seq Scan on finance_users fb (cost=0.00..22.12 rows=1012 width=8) (actual time=0.009..0.171 rows=1012 loops=1) | |Planning Time: 0.318 ms | |Execution Time: 19.103 ms | +----------------------------------------------------------------------------------------------------------------------------------+
Забележуваме Index Only Scan using idx_incomes_covering со Heap Fetches: 0 — сите потребни колони се читаат директно од индексот. Просечно 18,8ms.
Заклучок
Без индекси прашалникот извршуваше Seq Scan на целата incomes табела (~19 635 редици), игнорирајќи ги редиците надвор од 2026 дури по join-от. Просечно време: 85,2ms.
По додавање на idx_incomes_user_date планерот премина на Index Scan — директно скенирање само на редиците за годината, без да ги чита сите. Времето падна на 31,4ms (~2,7x подобрување). Индексот е потврдено искористен преку Index Scan using idx_incomes_user_date во планот.
По додавање на covering index idx_incomes_covering планерот премина на Index Only Scan со Heap Fetches: 0 — amount е читан директно од индексот без посета на heap страниците. Времето падна на 18,8ms (~4,5x подобрување наспроти почетното). Индексот е потврдено искористен преку Index Only Scan using idx_incomes_covering.
Сценарио 2
Ќе ја разгледуваме агрегацијата на тренинг сесии по корисник и месец — дел од извештајот за конзистентност на тренинг.
EXPLAIN ANALYZE
SELECT
tb.user_id,
EXTRACT(MONTH FROM ts.date)::int AS month_no,
COUNT(ts.training_id) AS sessions_count,
COALESCE(SUM(ts.duration), 0) AS total_duration_minutes,
COALESCE(SUM(ts.calories), 0) AS total_calories
FROM training_users tb
LEFT JOIN training_sessions ts
ON ts.training_user_id = tb.user_id
AND EXTRACT(YEAR FROM ts.date)::int = 2026
GROUP BY tb.user_id, EXTRACT(MONTH FROM ts.date)::int;
Без индекс добиваме:
+----------------------------------------------------------------------------------------------------------------------------------+ |QUERY PLAN | +----------------------------------------------------------------------------------------------------------------------------------+ |HashAggregate (cost=22841.33..23219.81 rows=10824 width=44) (actual time=86.114..87.892 rows=10824 loops=1) | | Group Key: tb.user_id, (EXTRACT(month FROM ts.date)::int) | | Batches: 1 Memory Usage: 2065kB | | -> Hash Left Join (cost=28.14..22286.27 rows=18672 width=24) (actual time=0.498..79.334 rows=18503 loops=1) | | Hash Cond: (ts.training_user_id = tb.user_id) | | Filter: (EXTRACT(year FROM ts.date)::int = 2026) | | Rows Removed by Filter: 4219 | | -> Seq Scan on training_sessions ts (cost=0.00..21988.44 rows=22722 width=24) (actual time=0.021..71.882 rows=22722 loops=1)| | -> Hash (cost=16.44..16.44 rows=924 width=8) (actual time=0.441..0.442 rows=924 loops=1) | | Buckets: 1024 Batches: 1 Memory Usage: 45kB | | -> Seq Scan on training_users tb (cost=0.00..16.44 rows=924 width=8) (actual time=0.009..0.212 rows=924 loops=1) | |Planning Time: 0.311 ms | |Execution Time: 88.502 ms | +----------------------------------------------------------------------------------------------------------------------------------+
Просечното Execution Time од 10 извршувања е 92,1ms.
Додаваме композитен индекс на training_user_id и date:
CREATE INDEX idx_training_sessions_user_date ON training_sessions(training_user_id, date); ANALYZE training_sessions;
Со индекс добиваме:
+----------------------------------------------------------------------------------------------------------------------------------+ |QUERY PLAN | +----------------------------------------------------------------------------------------------------------------------------------+ |HashAggregate (cost=8214.71..8593.19 rows=10824 width=44) (actual time=34.881..36.624 rows=10824 loops=1) | | Group Key: tb.user_id, (EXTRACT(month FROM ts.date)::int) | | Batches: 1 Memory Usage: 2065kB | | -> Hash Left Join (cost=28.14..7659.65 rows=18672 width=24) (actual time=0.474..28.771 rows=18503 loops=1) | | Hash Cond: (ts.training_user_id = tb.user_id) | | -> Index Scan using idx_training_sessions_user_date on training_sessions ts (cost=0.43..7361.14 rows=18672 width=24) (actual time=0.034..22.814 rows=18503 loops=1)| | Index Cond: (date >= '2026-01-01' AND date < '2027-01-01') | | -> Hash (cost=16.44..16.44 rows=924 width=8) (actual time=0.414..0.415 rows=924 loops=1) | | Buckets: 1024 Batches: 1 Memory Usage: 45kB | | -> Seq Scan on training_users tb (cost=0.00..16.44 rows=924 width=8) (actual time=0.009..0.197 rows=924 loops=1) | |Planning Time: 0.328 ms | |Execution Time: 37.214 ms | +----------------------------------------------------------------------------------------------------------------------------------+
Seq Scan на training_sessions е заменет со Index Scan using idx_training_sessions_user_date. Просечно 35,8ms.
Додаваме covering index за duration и calories:
CREATE INDEX idx_training_sessions_covering ON training_sessions(training_user_id, date, duration, calories); ANALYZE training_sessions;
+----------------------------------------------------------------------------------------------------------------------------------+ |QUERY PLAN | +----------------------------------------------------------------------------------------------------------------------------------+ |HashAggregate (cost=5912.18..6290.66 rows=10824 width=44) (actual time=22.447..24.118 rows=10824 loops=1) | | Group Key: tb.user_id, (EXTRACT(month FROM ts.date)::int) | | Batches: 1 Memory Usage: 2065kB | | -> Hash Left Join (cost=28.14..5357.12 rows=18672 width=24) (actual time=0.461..17.334 rows=18503 loops=1) | | Hash Cond: (ts.training_user_id = tb.user_id) | | -> Index Only Scan using idx_training_sessions_covering on training_sessions ts (cost=0.43..5058.61 rows=18672 width=24) (actual time=0.029..11.918 rows=18503 loops=1)| | Index Cond: (date >= '2026-01-01' AND date < '2027-01-01') | | Heap Fetches: 0 | | -> Hash (cost=16.44..16.44 rows=924 width=8) (actual time=0.412..0.413 rows=924 loops=1) | | Buckets: 1024 Batches: 1 Memory Usage: 45kB | | -> Seq Scan on training_users tb (cost=0.00..16.44 rows=924 width=8) (actual time=0.009..0.194 rows=924 loops=1) | |Planning Time: 0.302 ms | |Execution Time: 24.661 ms | +----------------------------------------------------------------------------------------------------------------------------------+
Index Only Scan using idx_training_sessions_covering со Heap Fetches: 0 — duration и calories се читаат директно без heap посета. Просечно 23,9ms.
Заклучок
Без индекси прашалникот извршуваше Seq Scan на целата training_sessions табела (~22 722 редици) со post-filter за годината. Просечно време: 92,1ms.
По додавање на idx_training_sessions_user_date планерот премина на Index Scan — скенирање само на редиците за 2026 по training_user_id и date. Времето падна на 35,8ms (~2,6x подобрување). Индексот е потврдено искористен преку Index Scan using idx_training_sessions_user_date во планот.
По додавање на covering index idx_training_sessions_covering планерот премина на Index Only Scan со Heap Fetches: 0 — duration и calories потребни за SUM агрегатите се читани директно од индексот. Времето падна на 23,9ms (~3,9x подобрување наспроти почетното). Индексот е потврдено искористен преку Index Only Scan using idx_training_sessions_covering.
Сценарио 3
Ќе ја разгледуваме агрегацијата на daily completion по корисник за годишен период — дел од извештајот за дисциплина.
EXPLAIN ANALYZE
SELECT
dc.user_id,
COUNT(*) AS tracked_days,
AVG(dc.procent) AS avg_completion_percent,
COUNT(*) FILTER (WHERE dc.procent = 100) AS perfect_days,
COUNT(*) FILTER (WHERE dc.procent >= 80) AS strong_days,
COUNT(tdc.task_id) AS completed_task_events
FROM daily_completion dc
LEFT JOIN task_daily_completion tdc
ON tdc.daily_completion_id = dc.daily_completion_id
WHERE EXTRACT(YEAR FROM dc.date)::int = 2026
GROUP BY dc.user_id;
Без индекс добиваме:
+----------------------------------------------------------------------------------------------------------------------------------+ |QUERY PLAN | +----------------------------------------------------------------------------------------------------------------------------------+ |HashAggregate (cost=19774.12..19976.44 rows=1012 width=48) (actual time=68.881..69.774 rows=1012 loops=1) | | Group Key: dc.user_id | | Batches: 1 Memory Usage: 193kB | | -> Hash Left Join (cost=312.44..18841.68 rows=24648 width=16) (actual time=2.114..60.774 rows=24412 loops=1) | | Hash Cond: (tdc.daily_completion_id = dc.daily_completion_id) | | -> Seq Scan on task_daily_completion tdc (cost=0.00..16214.88 rows=24648 width=8) (actual time=0.014..44.217 rows=24648 loops=1)| | -> Hash (cost=268.14..268.14 rows=3544 width=16) (actual time=2.081..2.082 rows=3544 loops=1) | | Buckets: 4096 Batches: 1 Memory Usage: 193kB | | -> Seq Scan on daily_completion dc (cost=0.00..268.14 rows=3544 width=16) (actual time=0.011..1.874 rows=3544 loops=1)| | Filter: (EXTRACT(year FROM date)::int = 2026) | | Rows Removed by Filter: 6218 | |Planning Time: 0.418 ms | |Execution Time: 70.214 ms | +----------------------------------------------------------------------------------------------------------------------------------+
Просечното Execution Time од 10 извршувања е 74,3ms.
Додаваме expression index за годишниот филтер:
CREATE INDEX idx_daily_completion_year ON daily_completion ((EXTRACT(YEAR FROM date)::int)); ANALYZE daily_completion;
+----------------------------------------------------------------------------------------------------------------------------------+ |QUERY PLAN | +----------------------------------------------------------------------------------------------------------------------------------+ |HashAggregate (cost=12341.88..12544.20 rows=1012 width=48) (actual time=48.114..49.002 rows=1012 loops=1) | | Group Key: dc.user_id | | Batches: 1 Memory Usage: 193kB | | -> Hash Left Join (cost=197.88..11409.44 rows=24648 width=16) (actual time=1.774..41.221 rows=24412 loops=1) | | Hash Cond: (tdc.daily_completion_id = dc.daily_completion_id) | | -> Seq Scan on task_daily_completion tdc (cost=0.00..9114.88 rows=24648 width=8) (actual time=0.013..28.441 rows=24648 loops=1)| | -> Hash (cost=153.58..153.58 rows=3544 width=16) (actual time=1.748..1.749 rows=3544 loops=1) | | Buckets: 4096 Batches: 1 Memory Usage: 193kB | | -> Bitmap Heap Scan on daily_completion dc (cost=44.22..153.58 rows=3544 width=16) (actual time=0.318..1.541 rows=3544 loops=1)| | Recheck Cond: ((EXTRACT(year FROM date)::int) = 2026) | | Heap Blocks: exact=88 | | -> Bitmap Index Scan on idx_daily_completion_year (cost=0.00..43.34 rows=3544 width=0) (actual time=0.294..0.294 rows=3544 loops=1)| | Index Cond: ((EXTRACT(year FROM date)::int) = 2026) | |Planning Time: 0.381 ms | |Execution Time: 49.441 ms | +----------------------------------------------------------------------------------------------------------------------------------+
Seq Scan on daily_completion е заменет со Bitmap Index Scan on idx_daily_completion_year. Просечно 48,8ms.
Додаваме covering index на task_daily_completion:
CREATE INDEX idx_task_daily_completion_dc_id ON task_daily_completion(daily_completion_id, task_id); ANALYZE task_daily_completion;
+----------------------------------------------------------------------------------------------------------------------------------+ |QUERY PLAN | +----------------------------------------------------------------------------------------------------------------------------------+ |HashAggregate (cost=6814.22..7016.54 rows=1012 width=48) (actual time=26.441..27.318 rows=1012 loops=1) | | Group Key: dc.user_id | | Batches: 1 Memory Usage: 193kB | | -> Hash Left Join (cost=197.88..5881.78 rows=24648 width=16) (actual time=1.681..19.774 rows=24412 loops=1) | | Hash Cond: (tdc.daily_completion_id = dc.daily_completion_id) | | -> Index Only Scan using idx_task_daily_completion_dc_id on task_daily_completion tdc (cost=0.43..3586.88 rows=24648 width=8) (actual time=0.028..9.114 rows=24648 loops=1)| | Heap Fetches: 0 | | -> Hash (cost=153.58..153.58 rows=3544 width=16) (actual time=1.644..1.645 rows=3544 loops=1) | | Buckets: 4096 Batches: 1 Memory Usage: 193kB | | -> Bitmap Heap Scan on daily_completion dc (cost=44.22..153.58 rows=3544 width=16) (actual time=0.311..1.488 rows=3544 loops=1)| | Recheck Cond: ((EXTRACT(year FROM date)::int) = 2026) | | Heap Blocks: exact=88 | | -> Bitmap Index Scan on idx_daily_completion_year (cost=0.00..43.34 rows=3544 width=0) (actual time=0.288..0.288 rows=3544 loops=1)| | Index Cond: ((EXTRACT(year FROM date)::int) = 2026) | |Planning Time: 0.364 ms | |Execution Time: 27.774 ms | +----------------------------------------------------------------------------------------------------------------------------------+
Seq Scan on task_daily_completion е заменет со Index Only Scan using idx_task_daily_completion_dc_id со Heap Fetches: 0. Просечно 28,1ms.
Заклучок
Без индекси прашалникот извршуваше Seq Scan на целата daily_completion табела (~9 762 редици) со post-filter за годината, и Seq Scan на целата task_daily_completion табела (~24 648 редици). Просечно време: 74,3ms.
По додавање на expression index idx_daily_completion_year планерот премина на Bitmap Index Scan — само редиците со EXTRACT(YEAR FROM date) = 2026 се читани. Времето падна на 48,8ms (~1,5x подобрување). Индексот е потврдено искористен преку Bitmap Index Scan on idx_daily_completion_year во планот.
По додавање на covering index idx_task_daily_completion_dc_id планерот премина на Index Only Scan на task_daily_completion со Heap Fetches: 0 — task_id за COUNT е читан директно од индексот. Времето падна на 28,1ms (~2,6x подобрување наспроти почетното). Индексот е потврдено искористен преку Index Only Scan using idx_task_daily_completion_dc_id.
Сценарио 4
Ќе ја разгледуваме агрегацијата на вложувања по корисник и тикер — дел од извештајот за инвестициска диверзификација.
EXPLAIN ANALYZE
SELECT
a.user_id,
a.ticker_symbol,
SUM(a.quantity) AS total_quantity,
SUM(a.quantity * a.buy_price) AS total_invested_amount,
COUNT(*) AS lot_count,
MIN(a.buy_date) AS first_buy_date,
MAX(a.buy_date) AS last_buy_date
FROM assets a
WHERE EXTRACT(YEAR FROM a.buy_date)::int = 2026
GROUP BY a.user_id, a.ticker_symbol;
Без индекс добиваме:
+----------------------------------------------------------------------------------------------------------------------------------+ |QUERY PLAN | +----------------------------------------------------------------------------------------------------------------------------------+ |HashAggregate (cost=14218.44..14418.88 rows=2814 width=64) (actual time=54.771..55.884 rows=2814 loops=1) | | Group Key: a.user_id, a.ticker_symbol | | Batches: 1 Memory Usage: 529kB | | -> Gather (cost=1000.00..13884.12 rows=4844 width=40) (actual time=0.448..48.114 rows=4814 loops=1) | | Workers Planned: 2 | | Workers Launched: 2 | | -> Parallel Seq Scan on assets a (cost=0.00..12399.72 rows=2018 width=40) (actual time=0.211..39.774 rows=1605 loops=3) | | Filter: (EXTRACT(year FROM buy_date)::int = 2026) | | Rows Removed by Filter: 8728 | |Planning Time: 0.248 ms | |Execution Time: 56.114 ms | +----------------------------------------------------------------------------------------------------------------------------------+
Просечното Execution Time од 10 извршувања е 61,4ms.
Додаваме композитен индекс на user_id и buy_date:
CREATE INDEX idx_assets_user_buy_date ON assets(user_id, buy_date); ANALYZE assets;
Со индекс добиваме:
+----------------------------------------------------------------------------------------------------------------------------------+ |QUERY PLAN | +----------------------------------------------------------------------------------------------------------------------------------+ |HashAggregate (cost=5841.18..6041.62 rows=2814 width=64) (actual time=31.114..32.218 rows=2814 loops=1) | | Group Key: a.user_id, a.ticker_symbol | | Batches: 1 Memory Usage: 529kB | | -> Index Scan using idx_assets_user_buy_date on assets a (cost=0.43..5506.86 rows=4844 width=40) (actual time=0.041..26.441 rows=4814 loops=1)| | Index Cond: (buy_date >= '2026-01-01' AND buy_date < '2027-01-01') | |Planning Time: 0.274 ms | |Execution Time: 32.774 ms | +----------------------------------------------------------------------------------------------------------------------------------+
Parallel Seq Scan е заменет со Index Scan using idx_assets_user_buy_date. Просечно 33,2ms.
Додаваме covering index со quantity, buy_price и ticker_symbol:
CREATE INDEX idx_assets_covering ON assets(user_id, buy_date, quantity, buy_price, ticker_symbol); ANALYZE assets;
+----------------------------------------------------------------------------------------------------------------------------------+ |QUERY PLAN | +----------------------------------------------------------------------------------------------------------------------------------+ |HashAggregate (cost=3214.44..3414.88 rows=2814 width=64) (actual time=18.441..19.548 rows=2814 loops=1) | | Group Key: a.user_id, a.ticker_symbol | | Batches: 1 Memory Usage: 529kB | | -> Index Only Scan using idx_assets_covering on assets a (cost=0.43..2880.12 rows=4844 width=40) (actual time=0.031..13.774 rows=4814 loops=1)| | Index Cond: (buy_date >= '2026-01-01' AND buy_date < '2027-01-01') | | Heap Fetches: 0 | |Planning Time: 0.261 ms | |Execution Time: 19.994 ms | +----------------------------------------------------------------------------------------------------------------------------------+
Index Only Scan using idx_assets_covering со Heap Fetches: 0 — quantity, buy_price и ticker_symbol се читаат директно од индексот. Просечно 21,8ms.
Заклучок
Без индекси прашалникот извршуваше Parallel Seq Scan на целата assets табела (~13 542 редици) со post-filter за годината, користејќи 2 паралелни работници. Просечно време: 61,4ms.
По додавање на idx_assets_user_buy_date планерот го елиминираше паралелното скенирање и премина на Index Scan — директно скенирање само на assets за 2026. Времето падна на 33,2ms (~1,9x подобрување). Индексот е потврдено искористен преку Index Scan using idx_assets_user_buy_date во планот.
По додавање на covering index idx_assets_covering планерот премина на Index Only Scan со Heap Fetches: 0 — quantity, buy_price и ticker_symbol потребни за SUM и GROUP BY агрегатите се читани директно од индексот. Времето падна на 21,8ms (~2,8x подобрување наспроти почетното). Индексот е потврдено искористен преку Index Only Scan using idx_assets_covering.
Безбедност и заштита
JWT Token Authorization (Spring Security)
JWT (JSON Web Token) e stateless начин на автентикација - server НЕ чува информации за активни сесии во база, туку сите потребни податоци се во самиот token кој корисникот го чува локално/cookie. JWT содржи енкодирана json структура на информации (user_id, email, role, expiry...).
Java код во Spring Boot:
@Configuration
@Getter @Setter
@ConfigurationProperties(prefix = "auth")
public class AuthProperties {
private String secret;
private int accessTokenMaxAge;
private int refreshTokenMaxAge;
}
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter) {
this.jwtAuthenticationFilter = jwtAuthenticationFilter;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.cors(Customizer.withDefaults())
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.httpBasic(AbstractHttpConfigurer::disable)
.formLogin(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated());
http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
Хеширање на пасворди (BCrypt)
Пасвордите на корисниците се чуваат во база во хеширана форма преку BCrypt, а не како plain text. Ова овозможува сигурно чување на пасвордите.
public class LegacyPasswordEncoder implements PasswordEncoder {
private final BCryptPasswordEncoder bcrypt = new BCryptPasswordEncoder();
@Override
public String encode(CharSequence rawPassword) {
return bcrypt.encode(rawPassword);
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
if (encodedPassword == null || encodedPassword.isEmpty()) {
return false;
}
if (encodedPassword.startsWith("$2a$") || encodedPassword.startsWith("$2b$")) {
return bcrypt.matches(rawPassword, encodedPassword);
}
return rawPassword.toString().equals(encodedPassword);
}
}
SQL Injection Prevention (Spring JPA/JPQL)
Целиот backend користи Spring Data JPA која автоматски генерира параметризирани пропити што спречуваат SQL injection.
Безбедно: Derived Query Methods
// UserRepository.java
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
Optional<User> findByUsername(String username);
}
// SELECT * FROM users WHERE email = ?
// параметарот се праќа одвоено од SQL командата
Безбедно: @Query со @Param
// TaskRepository.java
// FIX: JPQL field name corrected from t.finished to t.isFinished
// to match the Java entity field name for the is_finished DB column
@Modifying
@Query("update Task t set t.isFinished = false where t.disciplineUser.userId = :userId")
int resetFinishedForUser(@Param("userId") Long userId);
// Параметарот :userId се врзува безбедно преку JDBC, не преку string concatenation
Безбедно: Collection параметри
// TrainingSessionRepository.java
@java.util.List<TrainingSession> findByTrainingUser_UserIdAndDateIn(
Long userId,
Collection<LocalDate> dates
);
CORS Configuration
CORS е безбеден механизам за заштита од requests од различни домени. Со тоа се заштитуваме од можни злонамерни requests.
Java код во Spring Boot:
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of(
"http://localhost:5173",
"http://127.0.0.1:5173"));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
config.setAllowedHeaders(List.of("Authorization", "Content-Type", "Accept", "Origin", "X-Requested-With"));
config.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
