wiki:OtherTopics

Version 3 (modified by 231136, 2 weeks ago) ( diff )

--

Други развојни активности

Анализа на перформанси, додавање индекси

...

Безбедност и заштита

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 да се смени со соодветниот домен/и, а дополнително може и да се ограничат и дозволените методи и заглавја, но за рамките на овој проект сметаме дека ова е доволно.

Note: See TracWiki for help on using the wiki.