This commit is contained in:
OhSeongRak
2025-06-17 10:05:16 +09:00
commit 44d7312a85
178 changed files with 15106 additions and 0 deletions
+23
View File
@@ -0,0 +1,23 @@
bootJar {
enabled = false
}
jar {
enabled = true
archiveClassifier = ''
}
// 공통 의존성 재정의 (API 노출용)
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0'
implementation 'io.jsonwebtoken:jjwt-api:0.12.3'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.3'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.3'
implementation 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
}
@@ -0,0 +1,69 @@
package com.won.smarketing.common.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* Redis 설정 클래스
* Redis 연결 및 템플릿 설정
*/
@Configuration
public class RedisConfig {
@Value("${spring.data.redis.host}")
private String redisHost;
@Value("${spring.data.redis.port}")
private int redisPort;
@Value("${spring.data.redis.password:}")
private String redisPassword;
@Value("${spring.data.redis.ssl:true}")
private boolean useSsl;
/**
* Redis 연결 팩토리 설정
*
* @return Redis 연결 팩토리
*/
@Bean
public RedisConnectionFactory redisConnectionFactory() {
RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();
config.setHostName(redisHost);
config.setPort(redisPort);
// Azure Redis는 패스워드 인증 필수
if (redisPassword != null && !redisPassword.isEmpty()) {
config.setPassword(redisPassword);
}
LettuceConnectionFactory factory = new LettuceConnectionFactory(config);
// Azure Redis는 SSL 사용 (6380 포트)
factory.setUseSsl(useSsl);
factory.setValidateConnection(true);
return factory;
}
/**
* Redis 템플릿 설정
*
* @return Redis 템플릿
*/
@Bean
public RedisTemplate<String, String> redisTemplate() {
RedisTemplate<String, String> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory());
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
return redisTemplate;
}
}
@@ -0,0 +1,83 @@
package com.won.smarketing.common.config;
import com.won.smarketing.common.security.JwtAuthenticationFilter;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.Arrays;
/**
* Spring Security 설정 클래스
* JWT 기반 인증 및 CORS 설정
*/
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
/**
* Spring Security 필터 체인 설정
*
* @param http HttpSecurity 객체
* @return SecurityFilterChain
* @throws Exception 예외
*/
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**", "/api/member/register", "/api/member/check-duplicate/**",
"/api/member/validate-password", "/swagger-ui/**", "/v3/api-docs/**",
"/swagger-resources/**", "/webjars/**").permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
/**
* 패스워드 인코더 빈 등록
*
* @return BCryptPasswordEncoder
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* CORS 설정
*
* @return CorsConfigurationSource
*/
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOriginPatterns(Arrays.asList("*"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
configuration.setAllowedHeaders(Arrays.asList("*"));
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
@@ -0,0 +1,43 @@
package com.won.smarketing.common.config;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Swagger OpenAPI 설정 클래스
* API 문서화 및 JWT 인증 설정
*/
@Configuration
public class SwaggerConfig {
/**
* OpenAPI 설정
*
* @return OpenAPI 객체
*/
@Bean
public OpenAPI openAPI() {
String jwtSchemeName = "jwtAuth";
SecurityRequirement securityRequirement = new SecurityRequirement().addList(jwtSchemeName);
Components components = new Components()
.addSecuritySchemes(jwtSchemeName, new SecurityScheme()
.name(jwtSchemeName)
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT"));
return new OpenAPI()
.info(new Info()
.title("스마케팅 API")
.description("소상공인을 위한 AI 마케팅 서비스 API")
.version("1.0.0"))
.addSecurityItem(securityRequirement)
.components(components);
}
}
@@ -0,0 +1,77 @@
package com.won.smarketing.common.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 표준 API 응답 DTO
* 모든 API 응답에 사용되는 공통 형식
*
* @param <T> 응답 데이터 타입
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "API 응답")
public class ApiResponse<T> {
@Schema(description = "응답 상태 코드", example = "200")
private int status;
@Schema(description = "응답 메시지", example = "요청이 성공적으로 처리되었습니다.")
private String message;
@Schema(description = "응답 데이터")
private T data;
/**
* 성공 응답 생성 (데이터 포함)
*
* @param data 응답 데이터
* @param <T> 데이터 타입
* @return 성공 응답
*/
public static <T> ApiResponse<T> success(T data) {
return ApiResponse.<T>builder()
.status(200)
.message("요청이 성공적으로 처리되었습니다.")
.data(data)
.build();
}
/**
* 성공 응답 생성 (데이터 및 메시지 포함)
*
* @param data 응답 데이터
* @param message 응답 메시지
* @param <T> 데이터 타입
* @return 성공 응답
*/
public static <T> ApiResponse<T> success(T data, String message) {
return ApiResponse.<T>builder()
.status(200)
.message(message)
.data(data)
.build();
}
/**
* 오류 응답 생성
*
* @param status 오류 상태 코드
* @param message 오류 메시지
* @param <T> 데이터 타입
* @return 오류 응답
*/
public static <T> ApiResponse<T> error(int status, String message) {
return ApiResponse.<T>builder()
.status(status)
.message(message)
.data(null)
.build();
}
}
@@ -0,0 +1,68 @@
package com.won.smarketing.common.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* 페이징 응답 DTO
* 페이징된 데이터 응답에 사용되는 공통 형식
*
* @param <T> 응답 데이터 타입
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "페이징 응답")
public class PageResponse<T> {
@Schema(description = "페이지 컨텐츠", example = "[...]")
private List<T> content;
@Schema(description = "페이지 번호 (0부터 시작)", example = "0")
private int pageNumber;
@Schema(description = "페이지 크기", example = "20")
private int pageSize;
@Schema(description = "전체 요소 수", example = "100")
private long totalElements;
@Schema(description = "전체 페이지 수", example = "5")
private int totalPages;
@Schema(description = "첫 번째 페이지 여부", example = "true")
private boolean first;
@Schema(description = "마지막 페이지 여부", example = "false")
private boolean last;
/**
* 성공적인 페이징 응답 생성
*
* @param content 페이지 컨텐츠
* @param pageNumber 페이지 번호
* @param pageSize 페이지 크기
* @param totalElements 전체 요소 수
* @param <T> 데이터 타입
* @return 페이징 응답
*/
public static <T> PageResponse<T> of(List<T> content, int pageNumber, int pageSize, long totalElements) {
int totalPages = (int) Math.ceil((double) totalElements / pageSize);
return PageResponse.<T>builder()
.content(content)
.pageNumber(pageNumber)
.pageSize(pageSize)
.totalElements(totalElements)
.totalPages(totalPages)
.first(pageNumber == 0)
.last(pageNumber >= totalPages - 1)
.build();
}
}
@@ -0,0 +1,34 @@
package com.won.smarketing.common.exception;
import lombok.Getter;
/**
* 비즈니스 로직 예외
* 애플리케이션 내 비즈니스 규칙 위반 시 발생하는 예외
*/
@Getter
public class BusinessException extends RuntimeException {
private final ErrorCode errorCode;
/**
* 비즈니스 예외 생성자
*
* @param errorCode 오류 코드
*/
public BusinessException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.errorCode = errorCode;
}
/**
* 비즈니스 예외 생성자 (추가 메시지 포함)
*
* @param errorCode 오류 코드
* @param message 추가 메시지
*/
public BusinessException(ErrorCode errorCode, String message) {
super(message);
this.errorCode = errorCode;
}
}
@@ -0,0 +1,58 @@
package com.won.smarketing.common.exception;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
/**
* 애플리케이션 오류 코드 정의
* 각 오류 상황에 대한 코드, HTTP 상태, 메시지 정의
*/
@Getter
@RequiredArgsConstructor
public enum ErrorCode {
// 회원 관련 오류
MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "M001", "회원을 찾을 수 없습니다."),
DUPLICATE_MEMBER_ID(HttpStatus.BAD_REQUEST, "M002", "이미 사용 중인 사용자 ID입니다."),
DUPLICATE_EMAIL(HttpStatus.BAD_REQUEST, "M003", "이미 사용 중인 이메일입니다."),
DUPLICATE_BUSINESS_NUMBER(HttpStatus.BAD_REQUEST, "M004", "이미 등록된 사업자 번호입니다."),
INVALID_PASSWORD(HttpStatus.BAD_REQUEST, "M005", "잘못된 패스워드입니다."),
INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "M006", "유효하지 않은 토큰입니다."),
TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED, "M007", "만료된 토큰입니다."),
// 매장 관련 오류
STORE_NOT_FOUND(HttpStatus.NOT_FOUND, "S001", "매장을 찾을 수 없습니다."),
STORE_ALREADY_EXISTS(HttpStatus.BAD_REQUEST, "S002", "이미 등록된 매장이 있습니다."),
MENU_NOT_FOUND(HttpStatus.NOT_FOUND, "S003", "메뉴를 찾을 수 없습니다."),
// 마케팅 콘텐츠 관련 오류
CONTENT_NOT_FOUND(HttpStatus.NOT_FOUND, "C001", "콘텐츠를 찾을 수 없습니다."),
CONTENT_GENERATION_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "C002", "콘텐츠 생성에 실패했습니다."),
AI_SERVICE_UNAVAILABLE(HttpStatus.SERVICE_UNAVAILABLE, "C003", "AI 서비스를 사용할 수 없습니다."),
// AI 추천 관련 오류
RECOMMENDATION_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "R001", "추천 생성에 실패했습니다."),
EXTERNAL_API_ERROR(HttpStatus.SERVICE_UNAVAILABLE, "R002", "외부 API 호출에 실패했습니다."),
FILE_NOT_FOUND(HttpStatus.NOT_FOUND, "F001", "파일을 찾을 수 없습니다."),
FILE_UPLOAD_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "F002", "파일 업로드에 실패했습니다."),
FILE_SIZE_EXCEEDED(HttpStatus.NOT_FOUND, "F003", "파일 크기가 제한을 초과했습니다."),
INVALID_FILE_EXTENSION(HttpStatus.NOT_FOUND, "F004", "지원하지 않는 파일 확장자입니다."),
INVALID_FILE_TYPE(HttpStatus.NOT_FOUND, "F005", "지원하지 않는 파일 형식입니다."),
INVALID_FILE_NAME(HttpStatus.NOT_FOUND, "F006", "잘못된 파일명입니다."),
INVALID_FILE_URL(HttpStatus.NOT_FOUND, "F007", "잘못된 파일 URL입니다."),
STORAGE_CONTAINER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "F008", "스토리지 컨테이너 오류가 발생했습니다."),
// 공통 오류
INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "G001", "서버 내부 오류가 발생했습니다."),
INVALID_INPUT_VALUE(HttpStatus.BAD_REQUEST, "G002", "잘못된 입력값입니다."),
INVALID_TYPE_VALUE(HttpStatus.BAD_REQUEST, "G003", "잘못된 타입의 값입니다."),
MISSING_REQUEST_PARAMETER(HttpStatus.BAD_REQUEST, "G004", "필수 요청 파라미터가 누락되었습니다."),
ACCESS_DENIED(HttpStatus.FORBIDDEN, "G005", "접근이 거부되었습니다."),
METHOD_NOT_ALLOWED(HttpStatus.METHOD_NOT_ALLOWED, "G006", "허용되지 않은 HTTP 메서드입니다.");
private final HttpStatus httpStatus;
private final String code;
private final String message;
}
@@ -0,0 +1,79 @@
package com.won.smarketing.common.exception;
import com.won.smarketing.common.dto.ApiResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.HashMap;
import java.util.Map;
/**
* 전역 예외 처리기
* 애플리케이션 전반의 예외를 통일된 형식으로 처리
*/
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 비즈니스 예외 처리
*
* @param ex 비즈니스 예외
* @return 오류 응답
*/
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ApiResponse<Void>> handleBusinessException(BusinessException ex) {
log.warn("Business exception occurred: {}", ex.getMessage());
return ResponseEntity
.status(ex.getErrorCode().getHttpStatus())
.body(ApiResponse.error(
ex.getErrorCode().getHttpStatus().value(),
ex.getMessage()
));
}
/**
* 입력값 검증 예외 처리
*
* @param ex 입력값 검증 예외
* @return 오류 응답
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ApiResponse<Map<String, String>>> handleValidationException(
MethodArgumentNotValidException ex) {
log.warn("Validation exception occurred: {}", ex.getMessage());
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getAllErrors().forEach(error -> {
String fieldName = ((FieldError) error).getField();
String errorMessage = error.getDefaultMessage();
errors.put(fieldName, errorMessage);
});
return ResponseEntity.badRequest()
.body(ApiResponse.<Map<String, String>>builder()
.status(400)
.message("입력값 검증에 실패했습니다.")
.data(errors)
.build());
}
/**
* 일반적인 예외 처리
*
* @param ex 예외
* @return 오류 응답
*/
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiResponse<Void>> handleException(Exception ex) {
log.error("Unexpected exception occurred", ex);
return ResponseEntity.internalServerError()
.body(ApiResponse.error(500, "서버 내부 오류가 발생했습니다."));
}
}
@@ -0,0 +1,82 @@
package com.won.smarketing.common.security;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.Collections;
/**
* JWT 인증 필터
* HTTP 요청에서 JWT 토큰을 추출하고 인증 처리
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenProvider jwtTokenProvider;
private static final String AUTHORIZATION_HEADER = "Authorization";
private static final String BEARER_PREFIX = "Bearer ";
/**
* JWT 토큰 기반 인증 필터링
*
* @param request HTTP 요청
* @param response HTTP 응답
* @param filterChain 필터 체인
* @throws ServletException 서블릿 예외
* @throws IOException IO 예외
*/
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
try {
String jwt = getJwtFromRequest(request);
if (StringUtils.hasText(jwt) && jwtTokenProvider.validateToken(jwt)) {
String userId = jwtTokenProvider.getUserIdFromToken(jwt);
// 사용자 인증 정보 설정
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userId, null, Collections.emptyList());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
log.debug("User '{}' authenticated successfully", userId);
}
} catch (Exception ex) {
log.error("Could not set user authentication in security context", ex);
}
filterChain.doFilter(request, response);
}
/**
* HTTP 요청에서 JWT 토큰 추출
*
* @param request HTTP 요청
* @return JWT 토큰 (Bearer 접두사 제거된)
*/
private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader(AUTHORIZATION_HEADER);
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(BEARER_PREFIX)) {
return bearerToken.substring(BEARER_PREFIX.length());
}
return null;
}
}
@@ -0,0 +1,126 @@
package com.won.smarketing.common.security;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.util.Date;
/**
* JWT 토큰 생성 및 검증을 담당하는 클래스
* 액세스 토큰과 리프레시 토큰의 생성, 검증, 파싱 기능 제공
*/
@Slf4j
@Component
public class JwtTokenProvider {
private final SecretKey secretKey;
/**
* -- GETTER --
* 액세스 토큰 유효시간 반환
*
* @return 액세스 토큰 유효시간 (밀리초)
*/
@Getter
private final long accessTokenValidityTime;
private final long refreshTokenValidityTime;
/**
* JWT 토큰 프로바이더 생성자
*
* @param secret JWT 서명에 사용할 비밀키
* @param accessTokenValidityTime 액세스 토큰 유효시간 (밀리초)
* @param refreshTokenValidityTime 리프레시 토큰 유효시간 (밀리초)
*/
public JwtTokenProvider(@Value("${jwt.secret}") String secret,
@Value("${jwt.access-token-validity}") long accessTokenValidityTime,
@Value("${jwt.refresh-token-validity}") long refreshTokenValidityTime) {
this.secretKey = Keys.hmacShaKeyFor(secret.getBytes());
this.accessTokenValidityTime = accessTokenValidityTime;
this.refreshTokenValidityTime = refreshTokenValidityTime;
}
/**
* 액세스 토큰 생성
*
* @param userId 사용자 ID
* @return 생성된 액세스 토큰
*/
public String generateAccessToken(String userId) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + accessTokenValidityTime);
return Jwts.builder()
.subject(userId)
.issuedAt(now)
.expiration(expiryDate)
.signWith(secretKey)
.compact();
}
/**
* 리프레시 토큰 생성
*
* @param userId 사용자 ID
* @return 생성된 리프레시 토큰
*/
public String generateRefreshToken(String userId) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + refreshTokenValidityTime);
return Jwts.builder()
.subject(userId)
.issuedAt(now)
.expiration(expiryDate)
.signWith(secretKey)
.compact();
}
/**
* 토큰에서 사용자 ID 추출
*
* @param token JWT 토큰
* @return 사용자 ID
*/
public String getUserIdFromToken(String token) {
Claims claims = Jwts.parser()
.verifyWith(secretKey)
.build()
.parseSignedClaims(token)
.getPayload();
return claims.getSubject();
}
/**
* 토큰 유효성 검증
*
* @param token 검증할 토큰
* @return 유효성 여부
*/
public boolean validateToken(String token) {
try {
Jwts.parser()
.verifyWith(secretKey)
.build()
.parseSignedClaims(token);
return true;
} catch (SecurityException ex) {
log.error("Invalid JWT signature: {}", ex.getMessage());
} catch (MalformedJwtException ex) {
log.error("Invalid JWT token: {}", ex.getMessage());
} catch (ExpiredJwtException ex) {
log.error("Expired JWT token: {}", ex.getMessage());
} catch (UnsupportedJwtException ex) {
log.error("Unsupported JWT token: {}", ex.getMessage());
} catch (IllegalArgumentException ex) {
log.error("JWT claims string is empty: {}", ex.getMessage());
}
return false;
}
}