diff --git a/src/main/java/com/swyp/picke/domain/oauth/jwt/JwtFilter.java b/src/main/java/com/swyp/picke/domain/oauth/jwt/JwtFilter.java index 474f9158..e3172bcc 100644 --- a/src/main/java/com/swyp/picke/domain/oauth/jwt/JwtFilter.java +++ b/src/main/java/com/swyp/picke/domain/oauth/jwt/JwtFilter.java @@ -39,8 +39,9 @@ public class JwtFilter extends OncePerRequestFilter { "/favicon.ico", "/api/v1/auth/login", "/api/v1/auth/refresh", - "/swagger-ui", - "/v3/api-docs", + "/swagger-ui", + "/v3/api-docs", + "/webjars", "/api/v1/home", "/api/v1/notices", "/api/test", diff --git a/src/main/java/com/swyp/picke/global/config/S3Config.java b/src/main/java/com/swyp/picke/global/config/S3Config.java index f2e88717..923505ed 100644 --- a/src/main/java/com/swyp/picke/global/config/S3Config.java +++ b/src/main/java/com/swyp/picke/global/config/S3Config.java @@ -6,8 +6,11 @@ import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3Configuration; import software.amazon.awssdk.services.s3.presigner.S3Presigner; +import java.net.URI; + @Configuration public class S3Config { @@ -15,12 +18,22 @@ public class S3Config { public S3Presigner s3Presigner( @Value("${spring.cloud.aws.region.static}") String region, @Value("${spring.cloud.aws.credentials.access-key}") String accessKey, - @Value("${spring.cloud.aws.credentials.secret-key}") String secretKey) { + @Value("${spring.cloud.aws.credentials.secret-key}") String secretKey, + @Value("${spring.cloud.aws.s3.endpoint:}") String endpoint, + @Value("${spring.cloud.aws.s3.path-style-access-enabled:false}") boolean pathStyleAccessEnabled) { - return S3Presigner.builder() + S3Presigner.Builder builder = S3Presigner.builder() .region(Region.of(region)) .credentialsProvider(StaticCredentialsProvider.create( AwsBasicCredentials.create(accessKey, secretKey))) - .build(); + .serviceConfiguration(S3Configuration.builder() + .pathStyleAccessEnabled(pathStyleAccessEnabled) + .build()); + + if (!endpoint.isBlank()) { + builder.endpointOverride(URI.create(endpoint)); + } + + return builder.build(); } } diff --git a/src/main/java/com/swyp/picke/global/config/SecurityConfig.java b/src/main/java/com/swyp/picke/global/config/SecurityConfig.java index d0037617..d48f9003 100644 --- a/src/main/java/com/swyp/picke/global/config/SecurityConfig.java +++ b/src/main/java/com/swyp/picke/global/config/SecurityConfig.java @@ -44,6 +44,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { "/api/v1/notices/**", "/api/test/**", "/swagger-ui/**", "/swagger-ui.html", "/v3/api-docs/**", + "/webjars/**", "/js/**", "/css/**", "/favicon.ico", "/api/v1/admin/login", "/api/v1/admin", "/result/**", diff --git a/src/main/java/com/swyp/picke/global/infra/s3/service/S3PresignedUrlService.java b/src/main/java/com/swyp/picke/global/infra/s3/service/S3PresignedUrlService.java index 7edc0d70..4c23e88b 100644 --- a/src/main/java/com/swyp/picke/global/infra/s3/service/S3PresignedUrlService.java +++ b/src/main/java/com/swyp/picke/global/infra/s3/service/S3PresignedUrlService.java @@ -7,6 +7,7 @@ import software.amazon.awssdk.services.s3.presigner.S3Presigner; import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest; +import java.net.URI; import java.time.Duration; import java.util.Map; import java.util.stream.Collectors; @@ -54,11 +55,16 @@ public Map generatePresignedUrls(Map keyMap) { )); } + /** + * DB에는 보통 순수 key만 저장되지만, AWS S3를 쓰던 시절 저장된 레거시 데이터는 + * 전체 URL(https://{bucket}.s3.{region}.amazonaws.com/{key})일 수 있어 하위 호환을 위해 파싱한다. + * 스토리지 제공자(AWS/Railway 등)에 무관하게 동작하도록 호스트가 아닌 경로 기준으로 key를 뽑는다. + */ private String extractKey(String input) { - if (input.startsWith("https://") && input.contains(".s3.") && input.contains(".amazonaws.com/")) { - int idx = input.indexOf(".amazonaws.com/") + ".amazonaws.com/".length(); - return input.substring(idx); + if (!input.startsWith("http://") && !input.startsWith("https://")) { + return input; } - return input; + String path = URI.create(input).getPath(); + return path.startsWith("/") ? path.substring(1) : path; } } diff --git a/src/main/java/com/swyp/picke/global/infra/s3/service/S3UploadServiceImpl.java b/src/main/java/com/swyp/picke/global/infra/s3/service/S3UploadServiceImpl.java index 89004aaa..eb276bed 100644 --- a/src/main/java/com/swyp/picke/global/infra/s3/service/S3UploadServiceImpl.java +++ b/src/main/java/com/swyp/picke/global/infra/s3/service/S3UploadServiceImpl.java @@ -15,6 +15,7 @@ import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest; import java.io.File; +import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; import java.time.Duration; @@ -78,7 +79,7 @@ public File downloadFile(String fileUrl) { try { // URL에서 순수 Key만 추출 - String pureKey = fileUrl.contains(".com/") ? fileUrl.split(".com/")[1] : fileUrl; + String pureKey = extractKey(fileUrl); // 다운로드 받을 로컬 임시 파일 경로 생성 File tempFile = File.createTempFile("s3_download_", ".mp3"); @@ -112,7 +113,7 @@ public String getPresignedUrl(String key, Duration durationUrl) { if (key == null || key.isEmpty()) return null; // URL에서 도메인을 제외한 순수 'Key'만 추출 (만약 전체 URL이 들어올 경우를 대비) - String pureKey = key.contains(".com/") ? key.split(".com/")[1] : key; + String pureKey = extractKey(key); GetObjectPresignRequest presignRequest = GetObjectPresignRequest.builder() .signatureDuration(durationUrl) @@ -122,6 +123,18 @@ public String getPresignedUrl(String key, Duration durationUrl) { return s3Presigner.presignGetObject(presignRequest).url().toString(); } + /** + * DB/외부 입력에는 순수 key가 오는 게 기본이지만, 레거시 데이터나 실수로 전체 URL이 + * 들어오는 경우를 대비해 호스트가 아닌 경로 기준으로 key를 추출한다(스토리지 제공자 무관). + */ + private String extractKey(String input) { + if (!input.startsWith("http://") && !input.startsWith("https://")) { + return input; + } + String path = URI.create(input).getPath(); + return path.startsWith("/") ? path.substring(1) : path; + } + private String determineContentType(String key) { if (key.endsWith(".mp3")) return "audio/mpeg"; if (key.endsWith(".png")) return "image/png"; @@ -149,7 +162,7 @@ public void deleteFile(String fileUrl) { try { // 1. 전체 URL에서 순수 Key 추출 (기존 getPresignedUrl에 있던 방식과 동일하게 처리) - String pureKey = fileUrl.contains(".com/") ? fileUrl.split(".com/")[1] : fileUrl; + String pureKey = extractKey(fileUrl); // 2. AWS SDK v2 전용 삭제 요청 객체 생성 DeleteObjectRequest deleteObjectRequest = DeleteObjectRequest.builder() diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 60c036c8..faff467b 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -32,6 +32,8 @@ spring: aws: s3: bucket: ${AWS_S3_BUCKET} + endpoint: ${AWS_ENDPOINT_URL:} + path-style-access-enabled: ${S3_PATH_STYLE_ACCESS_ENABLED:false} region: static: ${AWS_REGION} credentials: