Changes between Initial Version and Version 1 of OtherTopics


Ignore:
Timestamp:
02/12/26 10:45:33 (2 weeks ago)
Author:
231136
Comment:

--

Legend:

Unmodified
Added
Removed
Modified
  • OtherTopics

    v1 v1  
     1= Други развојни активности
     2
     3== Анализа на перформанси, додавање индекси
     4
     5...
     6
     7== Безбедност и заштита
     8
     9=== JWT
     10
     11На најавени корисници на нашата апликација серверот им издава access token и refresh token.
     12
     13Access токенот е валиден многу кратко (во нашиот случај 10 минути) со цел дури и да е превземен од малициозен корисник истиот да стане бескорисен за брзо време. Овој токен е потребен за пристап до содржини и податоци коишто не се достапни за други корисници (пр. преглед/промена на лични податоци)
     14
     15Refresh токенот е валиден подолго време (14 дена) и се чува во база, за кога на корисникот ќе му истече access токенот тој да може да го докаже својот идентитет на серверот преку refresh токенот, и тогаш серверот да му издаде нов access токен.
     16
     17{{{
     18public String refreshAccessToken(String refreshTokenString) {
     19   RefreshToken refreshToken = refreshTokenService.validateRefreshToken(refreshTokenString);
     20   User user = refreshToken.getUser();
     21   return jwtService.generateToken(user.getUsername(), user.getRole().name());
     22}
     23}}}
     24
     25Проверка за валидност на refresh токен
     26{{{
     27public RefreshToken validateRefreshToken(String token){
     28   RefreshToken refreshToken = findByToken(token)
     29           .orElseThrow(() -> new InvalidTokenException("Invalid refresh token."));
     30   if (refreshToken.isRevoked()){
     31         throw new InvalidTokenException("Refresh token has been revoked.");
     32   }
     33   if (refreshToken.getExpiresAt().isBefore(Instant.now())){
     34         throw new InvalidTokenException("Refresh token has expired.");
     35   }
     36   return refreshToken;
     37}
     38}}}
     39
     40
     41Токенот е потпишан од серверот користејќи таен клуч со цел да не може малициозен корисник сам да си издаде свој токен.
     42Метода за генерирање токен:
     43
     44{{{
     45public String generateToken(String username, String role){
     46    SecretKey key = Keys.hmacShaKeyFor(authProperties.getSecret().getBytes(StandardCharsets.UTF_8));
     47    return Jwts.builder()
     48            .setSubject(username)
     49            .claim("role", "ROLE_" + role)
     50            .setExpiration(new Date(System.currentTimeMillis() + (long) authProperties.getAccessTokenMaxAge() * 1000))
     51            .signWith(key)
     52            .compact();
     53}
     54}}}
     55
     56Дополнително, имплементираме наш филтер кој ќе го проверува постоењето и валидноста на горенаведениот access токен
     57{{{
     58@Component
     59@RequiredArgsConstructor
     60public class JwtFilter extends OncePerRequestFilter {
     61    private final CustomUserDetailsService userDetailsService;
     62    private final JwtService jwtService;
     63
     64    @Override
     65    protected void doFilterInternal(
     66            @NonNull HttpServletRequest request,
     67            @NonNull HttpServletResponse response,
     68            @NonNull FilterChain filterChain) throws ServletException, IOException {
     69       
     70        String token = null;
     71        if (request.getCookies() != null){
     72            for (Cookie cookie: request.getCookies()){
     73                if ("accessToken".equals(cookie.getName())){
     74                    token = cookie.getValue();
     75                    break;
     76                }
     77            }
     78        }
     79        if (token != null && !token.isEmpty()){
     80            try {
     81                Claims claims = jwtService.extractClaims(token);
     82                String username = claims.getSubject();
     83                if (username != null && SecurityContextHolder.getContext().getAuthentication() == null){
     84                    UserDetails userDetails = userDetailsService.loadUserByUsername(username);
     85                    UsernamePasswordAuthenticationToken authToken =
     86                            new UsernamePasswordAuthenticationToken(username, null, userDetails.getAuthorities());
     87                    SecurityContextHolder.getContext().setAuthentication(authToken);
     88                }
     89            } catch (ExpiredJwtException e){
     90                System.out.println("Expired jwt token.");
     91                response.setStatus(HttpStatus.UNAUTHORIZED.value());
     92                return;
     93            } catch (JwtException e){
     94                System.out.println("Invalid jwt token.");
     95                response.setStatus(HttpStatus.UNAUTHORIZED.value());
     96                return;
     97            } catch (Exception e){
     98                System.out.println(e.getMessage());
     99                response.setStatus(HttpStatus.UNAUTHORIZED.value());
     100                return;
     101            }
     102        }
     103        filterChain.doFilter(request, response);
     104    }
     105}
     106}}}
     107
     108Потоа, истиот треба да го додадеме во security конфигурацијата
     109{{{
     110@Bean
     111    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
     112        http
     113                .cors(cors -> cors.configurationSource(corsConfigurationSource()))
     114                .csrf(AbstractHttpConfigurer::disable)
     115                .authorizeHttpRequests(auth -> auth
     116                    .requestMatchers("/auth/user").authenticated()
     117                    .anyRequest().permitAll())
     118                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
     119                .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class) // <---- оваа линија е битна !!!!
     120                .headers((headers) ->
     121                        headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin));
     122        return http.build();
     123    }
     124}}}
     125
     126=== Чување лозинки
     127
     128Бидејќи чуваме лозинки во нашата база не смееме истите да ги чуваме како plaintext.
     129
     130Корстиме `BCryptPasswordEncoder` за хеширање на истите:
     131{{{
     132@Bean
     133    public PasswordEncoder passwordEncoder(){
     134    return new BCryptPasswordEncoder(10);
     135}
     136
     137// понатаму кога создаваме нов корисник
     138User.UserBuilder userBuilder = User.builder()
     139    .username(authRequestDto.username())
     140    .password(passwordEncoder.encode(authRequestDto.password()))
     141// ... останата логика
     142}}}
     143
     144=== SQL Injection
     145
     146За да намалиме можност за SQL injection користиме параметизирани прашалници, наместо да ги додаваме параметрите со конкатенација.
     147
     148Всушност, бидејќи користиме Spring Data JPA / JPQL овие работи не треба експлицитно да ги пишуваме, туку се веќе имплементирани:
     149
     150пример:
     151{{{
     152@Query("""
     153    SELECT CASE WHEN COUNT (l)>0 THEN true ELSE false END
     154    FROM MusicalEntity me
     155    JOIN Like l on l.musicalEntity.id=me.id
     156    WHERE l.listener.id=:userId
     157""")
     158boolean isLikedByUser(@Param("userId") Long userId);
     159}}}
     160
     161=== CORS
     162
     163Конфигурација за CORS
     164{{{
     165@Bean
     166public CorsConfigurationSource corsConfigurationSource() {
     167    CorsConfiguration configuration = new CorsConfiguration();
     168    configuration.setAllowedOriginPatterns(List.of("http://localhost:*"));
     169    configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
     170    configuration.setAllowedHeaders(List.of("*"));
     171    configuration.setAllowCredentials(true);
     172    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
     173    source.registerCorsConfiguration("/**", configuration);
     174    return source;
     175}
     176}}}
     177
     178Во продукциска околина, секако дека ќе треба `localhost` да се смени со соодветниот домен/и, а дополнително може и да се ограничат и дозволените методи и заглавја, но за рамките на овој проект сметаме дека ова е доволно.