perf: azure deployment optimizations

This commit is contained in:
2026-04-15 16:25:31 -06:00
parent e87bb7bebf
commit f50928fef1
12 changed files with 177 additions and 29 deletions

View File

@@ -4,10 +4,12 @@ import com.petshop.backend.config.FlywayContextInitializer;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.web.config.EnableSpringDataWebSupport;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableScheduling
@EnableAsync
@EnableSpringDataWebSupport(pageSerializationMode = EnableSpringDataWebSupport.PageSerializationMode.VIA_DTO)
public class BackendApplication {
public static void main(String[] args) {

View File

@@ -0,0 +1,24 @@
package com.petshop.backend.config;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.TimeUnit;
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager manager = new CaffeineCacheManager("userAuthCache");
manager.setCaffeine(Caffeine.newBuilder()
.expireAfterWrite(60, TimeUnit.SECONDS)
.maximumSize(1000));
return manager;
}
}

View File

@@ -15,6 +15,7 @@ import com.petshop.backend.entity.StoreLocation;
import com.petshop.backend.entity.User;
import com.petshop.backend.repository.UserRepository;
import com.petshop.backend.security.JwtUtil;
import com.petshop.backend.security.UserAuthCacheService;
import com.petshop.backend.service.ActivityLogService;
import com.petshop.backend.service.AvatarStorageService;
import com.petshop.backend.service.EmailService;
@@ -58,8 +59,9 @@ public class AuthController {
private final ActivityLogService activityLogService;
private final PasswordResetService passwordResetService;
private final EmailService emailService;
private final UserAuthCacheService userAuthCacheService;
public AuthController(AuthenticationManager authenticationManager, UserRepository userRepository, JwtUtil jwtUtil, PasswordEncoder passwordEncoder, AvatarStorageService avatarStorageService, ActivityLogService activityLogService, PasswordResetService passwordResetService, EmailService emailService) {
public AuthController(AuthenticationManager authenticationManager, UserRepository userRepository, JwtUtil jwtUtil, PasswordEncoder passwordEncoder, AvatarStorageService avatarStorageService, ActivityLogService activityLogService, PasswordResetService passwordResetService, EmailService emailService, UserAuthCacheService userAuthCacheService) {
this.authenticationManager = authenticationManager;
this.userRepository = userRepository;
this.jwtUtil = jwtUtil;
@@ -68,6 +70,7 @@ public class AuthController {
this.activityLogService = activityLogService;
this.passwordResetService = passwordResetService;
this.emailService = emailService;
this.userAuthCacheService = userAuthCacheService;
}
@PostMapping("/register")
@@ -263,6 +266,7 @@ public class AuthController {
error.put("message", "Username, email, or phone already exists");
return ResponseEntity.status(HttpStatus.CONFLICT).body(error);
}
userAuthCacheService.evict(updatedUser.getId());
return ResponseEntity.ok(toUserInfoResponse(updatedUser));
}

View File

@@ -2,7 +2,6 @@ package com.petshop.backend.security;
import com.petshop.backend.entity.User;
import com.petshop.backend.exception.ApiErrorResponder;
import com.petshop.backend.repository.UserRepository;
import io.jsonwebtoken.JwtException;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
@@ -16,16 +15,18 @@ import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.Date;
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
private final UserRepository userRepository;
private final UserAuthCacheService userAuthCacheService;
private final ApiErrorResponder apiErrorResponder;
public JwtAuthenticationFilter(JwtUtil jwtUtil, UserRepository userRepository, ApiErrorResponder apiErrorResponder) {
public JwtAuthenticationFilter(JwtUtil jwtUtil, UserAuthCacheService userAuthCacheService, ApiErrorResponder apiErrorResponder) {
this.jwtUtil = jwtUtil;
this.userRepository = userRepository;
this.userAuthCacheService = userAuthCacheService;
this.apiErrorResponder = apiErrorResponder;
}
@@ -44,30 +45,44 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
jwt = authHeader.substring(7);
Long userId;
String username;
String roleStr;
Integer jwtTokenVersion;
try {
userId = jwtUtil.extractUserId(jwt);
username = jwtUtil.extractUsername(jwt);
roleStr = jwtUtil.extractRole(jwt);
jwtTokenVersion = jwtUtil.extractTokenVersion(jwt);
} catch (JwtException | IllegalArgumentException ex) {
writeUnauthorized(request, response, "Invalid or expired token", ex);
return;
}
if (userId != null && SecurityContextHolder.getContext().getAuthentication() == null) {
User user = userRepository.findById(userId).orElse(null);
if (user == null || user.getActive() == null || !user.getActive()) {
writeUnauthorized(request, response, "User account is inactive", null);
return;
}
if (!jwtUtil.validateToken(jwt, user)) {
if (jwtUtil.extractExpiration(jwt).before(new Date())) {
writeUnauthorized(request, response, "Invalid or expired token", null);
return;
}
AppPrincipal principal = new AppPrincipal(
user.getId(),
user.getUsername(),
user.getRole(),
user.getTokenVersion()
);
UserAuthCacheService.UserAuthData authData = userAuthCacheService.loadAuthData(userId);
if (authData == null || !Boolean.TRUE.equals(authData.active())) {
writeUnauthorized(request, response, "User account is inactive", null);
return;
}
if (!authData.tokenVersion().equals(jwtTokenVersion)) {
writeUnauthorized(request, response, "Invalid or expired token", null);
return;
}
User.Role role;
try {
role = User.Role.valueOf(roleStr);
} catch (IllegalArgumentException ex) {
writeUnauthorized(request, response, "Invalid or expired token", ex);
return;
}
AppPrincipal principal = new AppPrincipal(userId, username, role, jwtTokenVersion);
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
principal,
null,

View File

@@ -0,0 +1,29 @@
package com.petshop.backend.security;
import com.petshop.backend.repository.UserRepository;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
@Service
public class UserAuthCacheService {
private final UserRepository userRepository;
public UserAuthCacheService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public record UserAuthData(Boolean active, Integer tokenVersion) {}
@Cacheable(value = "userAuthCache", key = "#userId")
public UserAuthData loadAuthData(Long userId) {
return userRepository.findById(userId)
.map(u -> new UserAuthData(u.getActive(), u.getTokenVersion()))
.orElse(null);
}
@CacheEvict(value = "userAuthCache", key = "#userId")
public void evict(Long userId) {
}
}

View File

@@ -5,6 +5,7 @@ import com.petshop.backend.entity.User;
import com.petshop.backend.repository.UserRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
@Service
@@ -18,6 +19,7 @@ public class ActivityLogService {
this.userRepository = userRepository;
}
@Async
public void record(Long userId, String activity) {
if (userId == null || activity == null || activity.isBlank()) {
return;
@@ -36,6 +38,7 @@ public class ActivityLogService {
}
}
@Async
public void record(User user, String activity) {
if (user == null) {
return;

View File

@@ -7,6 +7,7 @@ import com.petshop.backend.entity.User;
import com.petshop.backend.exception.BusinessException;
import com.petshop.backend.repository.PasswordResetTokenRepository;
import com.petshop.backend.repository.UserRepository;
import com.petshop.backend.security.UserAuthCacheService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -29,16 +30,19 @@ public class PasswordResetService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final EmailService emailService;
private final UserAuthCacheService userAuthCacheService;
private final SecureRandom secureRandom = new SecureRandom();
public PasswordResetService(PasswordResetTokenRepository passwordResetTokenRepository,
UserRepository userRepository,
PasswordEncoder passwordEncoder,
EmailService emailService) {
EmailService emailService,
UserAuthCacheService userAuthCacheService) {
this.passwordResetTokenRepository = passwordResetTokenRepository;
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
this.emailService = emailService;
this.userAuthCacheService = userAuthCacheService;
}
@Transactional
@@ -97,6 +101,7 @@ public class PasswordResetService {
user.setPassword(passwordEncoder.encode(newPassword));
user.setTokenVersion(user.getTokenVersion() + 1);
userRepository.save(user);
userAuthCacheService.evict(user.getId());
token.setUsedAt(now);
passwordResetTokenRepository.save(token);

View File

@@ -8,6 +8,7 @@ import com.petshop.backend.entity.User;
import com.petshop.backend.exception.ResourceNotFoundException;
import com.petshop.backend.repository.StoreRepository;
import com.petshop.backend.repository.UserRepository;
import com.petshop.backend.security.UserAuthCacheService;
import com.petshop.backend.util.AuthenticationHelper;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
@@ -33,11 +34,13 @@ public class UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final StoreRepository storeRepository;
private final UserAuthCacheService userAuthCacheService;
public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder, StoreRepository storeRepository) {
public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder, StoreRepository storeRepository, UserAuthCacheService userAuthCacheService) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
this.storeRepository = storeRepository;
this.userAuthCacheService = userAuthCacheService;
}
public Page<UserResponse> getAllUsers(String query, String role, Pageable pageable) {
@@ -147,6 +150,7 @@ public class UserService {
}
user = userRepository.save(user);
userAuthCacheService.evict(user.getId());
return mapToResponse(user);
}

View File

@@ -18,6 +18,10 @@ spring:
username: ${SPRING_DATASOURCE_USERNAME:petshop}
password: ${SPRING_DATASOURCE_PASSWORD:petshop}
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 30000
sql:
init:
@@ -42,6 +46,10 @@ server:
address: ${SERVER_ADDRESS:0.0.0.0}
servlet:
context-path: /
compression:
enabled: true
mime-types: application/json,application/javascript,text/css,text/plain
min-response-size: 1024
springdoc:
api-docs: