= Други развојни активности == Анализа на перформанси, додавање индекси ... == Безбедност и заштита === JWT На најавени корисници на нашата апликација серверот им издава access token и refresh token. Access токенот е валиден многу кратко (во нашиот случај 10 минути) со цел дури и да е превземен од малициозен корисник истиот да стане бескорисен за брзо време. Овој токен е потребен за пристап до содржини и податоци коишто не се достапни за други корисници (пр. преглед/промена на лични податоци) Refresh токенот е валиден подолго време (14 дена) и се чува во база, за кога на корисникот ќе му истече access токенот тој да може да го докаже својот идентитет на серверот преку refresh токенот, и тогаш серверот да му издаде нов access токен. {{{ public String refreshAccessToken(String refreshTokenString) { RefreshToken refreshToken = refreshTokenService.validateRefreshToken(refreshTokenString); User user = refreshToken.getUser(); return jwtService.generateToken(user.getUsername(), user.getRole().name()); } }}} Проверка за валидност на refresh токен {{{ public RefreshToken validateRefreshToken(String token){ RefreshToken refreshToken = findByToken(token) .orElseThrow(() -> new InvalidTokenException("Invalid refresh token.")); if (refreshToken.isRevoked()){ throw new InvalidTokenException("Refresh token has been revoked."); } if (refreshToken.getExpiresAt().isBefore(Instant.now())){ throw new InvalidTokenException("Refresh token has expired."); } return refreshToken; } }}} Токенот е потпишан од серверот користејќи таен клуч со цел да не може малициозен корисник сам да си издаде свој токен. Метода за генерирање токен: {{{ public String generateToken(String username, String role){ SecretKey key = Keys.hmacShaKeyFor(authProperties.getSecret().getBytes(StandardCharsets.UTF_8)); return Jwts.builder() .setSubject(username) .claim("role", "ROLE_" + role) .setExpiration(new Date(System.currentTimeMillis() + (long) authProperties.getAccessTokenMaxAge() * 1000)) .signWith(key) .compact(); } }}} Дополнително, имплементираме наш филтер кој ќе го проверува постоењето и валидноста на горенаведениот access токен {{{ @Component @RequiredArgsConstructor public class JwtFilter extends OncePerRequestFilter { private final CustomUserDetailsService userDetailsService; private final JwtService jwtService; @Override protected void doFilterInternal( @NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain) throws ServletException, IOException { String token = null; if (request.getCookies() != null){ for (Cookie cookie: request.getCookies()){ if ("accessToken".equals(cookie.getName())){ token = cookie.getValue(); break; } } } if (token != null && !token.isEmpty()){ try { Claims claims = jwtService.extractClaims(token); String username = claims.getSubject(); if (username != null && SecurityContextHolder.getContext().getAuthentication() == null){ UserDetails userDetails = userDetailsService.loadUserByUsername(username); UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(username, null, userDetails.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authToken); } } catch (ExpiredJwtException e){ System.out.println("Expired jwt token."); response.setStatus(HttpStatus.UNAUTHORIZED.value()); return; } catch (JwtException e){ System.out.println("Invalid jwt token."); response.setStatus(HttpStatus.UNAUTHORIZED.value()); return; } catch (Exception e){ System.out.println(e.getMessage()); response.setStatus(HttpStatus.UNAUTHORIZED.value()); return; } } filterChain.doFilter(request, response); } } }}} Потоа, истиот треба да го додадеме во security конфигурацијата {{{ @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .cors(cors -> cors.configurationSource(corsConfigurationSource())) .csrf(AbstractHttpConfigurer::disable) .authorizeHttpRequests(auth -> auth .requestMatchers("/auth/user").authenticated() .anyRequest().permitAll()) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class) // <---- оваа линија е битна !!!! .headers((headers) -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin)); return http.build(); } }}} === Чување лозинки Бидејќи чуваме лозинки во нашата база не смееме истите да ги чуваме како plaintext. Корстиме `BCryptPasswordEncoder` за хеширање на истите: {{{ @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(10); } // понатаму кога создаваме нов корисник User.UserBuilder userBuilder = User.builder() .username(authRequestDto.username()) .password(passwordEncoder.encode(authRequestDto.password())) // ... останата логика }}} === SQL Injection За да намалиме можност за SQL injection користиме параметизирани прашалници, наместо да ги додаваме параметрите со конкатенација. Всушност, бидејќи користиме Spring Data JPA / JPQL овие работи не треба експлицитно да ги пишуваме, туку се веќе имплементирани: пример: {{{ @Query(""" SELECT CASE WHEN COUNT (l)>0 THEN true ELSE false END FROM MusicalEntity me JOIN Like l on l.musicalEntity.id=me.id WHERE l.listener.id=:userId """) boolean isLikedByUser(@Param("userId") Long userId); }}} === CORS Конфигурација за CORS {{{ @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); configuration.setAllowedOriginPatterns(List.of("http://localhost:*")); configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS")); configuration.setAllowedHeaders(List.of("*")); configuration.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", configuration); return source; } }}} Во продукциска околина, секако дека ќе треба `localhost` да се смени со соодветниот домен/и, а дополнително може и да се ограничат и дозволените методи и заглавја, но за рамките на овој проект сметаме дека ова е доволно.