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