Skip to content

Commit

Permalink
Merge pull request #98 from wafflestudio/dev
Browse files Browse the repository at this point in the history
s3 배포 테스트 위한 main merge
  • Loading branch information
JunBye authored Jan 13, 2025
2 parents a4c0987 + 69626b9 commit 75944f4
Show file tree
Hide file tree
Showing 6 changed files with 200 additions and 0 deletions.
7 changes: 7 additions & 0 deletions .github/workflows/cd-aws.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@ jobs:
echo "${{ secrets.APPLICATION }}" > ./src/main/resources/application.yaml
echo "${{ secrets.P12_BASE64 }}" | base64 --decode > ./src/main/resources/test.p12
- name: Set up CloudFront environment variables
env:
CLF_PRIVATE_KEY: ${{ secrets.CLF_PRIVATE_KEY }}
CLF_KEY_PAIR_ID: ${{ secrets.CLF_KEY_PAIR_ID }}
run: echo "Environment variables set"


- name: Build with Gradle
run: ./gradlew build -x test

Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ jobs:
build-and-test: # Job 이름
runs-on: ubuntu-latest # 작업 실행 환경

env:
# GitHub Secrets 주입
CLOUDFRONT_PRIVATE_KEY: ${{ secrets.CLF_PRIVATE_KEY }}
CLOUDFRONT_KEY_PAIR_ID: ${{ secrets.CLF_KEY_PAIR_ID }}

steps:
- name: Checkout code # Step 이름
uses: actions/checkout@v4 # GitHub Action 사용 (코드 체크아웃)
Expand Down
3 changes: 3 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ dependencies {
implementation("org.springframework.boot:spring-boot-starter-oauth2-client")
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("io.github.oshai:kotlin-logging-jvm:5.1.1")
implementation("org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE")
implementation("software.amazon.awssdk:s3:2.20.32")
implementation("com.amazonaws:aws-java-sdk-cloudfront:1.12.592") // 최신 버전 사용, signed URL 생성을 위해 필요
}

kotlin {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package com.example.toyTeam6Airbnb.Image

import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.multipart.MultipartFile
import java.nio.file.Paths
import java.util.Date

@RestController
@RequestMapping("/api/images")
class ImageController(
private val imageService: ImageService
) {

// 이미지 업로드 엔드포인트
@PostMapping("/upload")
fun uploadImage(
@RequestParam("file") file: MultipartFile,
@RequestParam("key") key: String
): ResponseEntity<String> {
return try {
val tempFile = Paths.get(System.getProperty("java.io.tmpdir"), file.originalFilename).toFile()
file.transferTo(tempFile) // MultipartFile을 임시 파일로 저장

imageService.uploadFile(tempFile.absolutePath, key) // S3에 업로드
tempFile.delete() // 임시 파일 삭제

ResponseEntity("Image uploaded successfully with key: $key", HttpStatus.OK)
} catch (e: Exception) {
ResponseEntity("Failed to upload image: ${e.message}", HttpStatus.INTERNAL_SERVER_ERROR)
}
}

@GetMapping("/{key}")
fun getImageSignedUrl(
@PathVariable("key") key: String
): ResponseEntity<String> {
return try {
val expirationDate = Date(System.currentTimeMillis() + 3600_000) // 1시간 유효

val signedUrl = imageService.generateSignedUrl(
"https://d3m9s5wmwvsq01.cloudfront.net",
key,
expirationDate
)

ResponseEntity.ok(signedUrl)
} catch (e: Exception) {
ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("Failed to generate signed URL: ${e.message}")
}
}
}

// // 이미지 다운로드 엔드포인트
// @GetMapping("/download")
// fun downloadImage(
// @RequestParam("key") key: String,
// @RequestParam("destination") destination: String
// ): ResponseEntity<String> {
// return try {
// imageService.downloadFile(key, destination) // S3에서 다운로드
// ResponseEntity("Image downloaded successfully to: $destination", HttpStatus.OK)
// } catch (e: Exception) {
// ResponseEntity("Failed to download image: ${e.message}", HttpStatus.INTERNAL_SERVER_ERROR)
// }
// }

// // 이미지를 화면에 표시, 보안 취약
// @GetMapping("/{key}")
// fun getImage(
// @PathVariable("key") key: String
// ): ResponseEntity<Void> {
// return try {
// // CloudFront 배포 URL
// val cloudFrontUrl = "https://d3m9s5wmwvsq01.cloudfront.net/$key"
//
// // 302 Redirect
// ResponseEntity.status(HttpStatus.FOUND)
// .header("Location", cloudFrontUrl)
// .build()
// } catch (e: Exception) {
// ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
// .body(null)
// }
// }
92 changes: 92 additions & 0 deletions src/main/kotlin/com/example/toyTeam6Airbnb/Image/ImageService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package com.example.toyTeam6Airbnb.Image

import com.amazonaws.services.cloudfront.CloudFrontUrlSigner
import com.amazonaws.services.cloudfront.util.SignerUtils
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Service
import software.amazon.awssdk.core.sync.RequestBody
import software.amazon.awssdk.regions.Region
import software.amazon.awssdk.services.s3.S3Client
import software.amazon.awssdk.services.s3.model.PutObjectRequest
import java.io.File
import java.nio.file.Paths
import java.security.PrivateKey
import java.util.Date

@Service
class ImageService() {

@Value("\${cloudfront.private-key}")
private lateinit var privateKey: String

@Value("\${cloudfront.key-pair-id}")
private lateinit var keyPairId: String
private val s3Client: S3Client = S3Client.builder()
.region(Region.AP_NORTHEAST_2) // 원하는 리전 설정
.build()
private val bucketName: String = "waffle-team6-storage"
private val cloudFrontUrl: String = "https://d3m9s5wmwvsq01.cloudfront.net"

// 파일 업로드 메서드
fun uploadFile(filePath: String, key: String) {
s3Client.putObject(
PutObjectRequest.builder()
.bucket(bucketName)
.key(key)
.build(),
RequestBody.fromFile(Paths.get(filePath))
)
}

// // 파일 다운로드 메서드
// @Transactional
// fun downloadFile(key: String, destination: String) {
// s3Client.getObject(
// GetObjectRequest.builder()
// .bucket(bucketName)
// .key(key)
// .build(),
// Paths.get(destination)
// )
// }

// CloudFront signed URL 생성 메서드
fun generateSignedUrl(
domain: String,
key: String,
expiration: Date
): String {
// privateKey와 keyPairId는 GitHub Secrets에서 가져온 값이 자동으로 주입됨
return createSignedUrl(domain, key, privateKey, keyPairId, expiration)
}

// CloudFront 서명 URL 생성 메서드
// createSignedUrl 메서드 구현
private fun createSignedUrl(
domain: String,
key: String,
privateKey: String,
keyPairId: String,
expiration: Date
): String {
try {
// 1. 임시 파일 생성 및 비공개 키 쓰기
val tempFile = File.createTempFile("privateKey", ".pem")
tempFile.deleteOnExit() // 애플리케이션 종료 시 자동 삭제
tempFile.writeText(privateKey)

// 2. Private Key 객체 로드
val privateKeyObj: PrivateKey = SignerUtils.loadPrivateKey(tempFile)

// 3. CloudFront 서명된 URL 생성
return CloudFrontUrlSigner.getSignedURLWithCannedPolicy(
"$domain/$key", // 리소스 URL
keyPairId, // Key Pair ID
privateKeyObj, // PrivateKey 객체
expiration // 만료 시간
)
} catch (e: Exception) {
throw RuntimeException("Failed to create signed URL: ${e.message}", e)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ class SecurityConfig(
authorize(HttpMethod.GET, "/api/v1/reviews/**", permitAll)
authorize("/error", permitAll)
authorize("/redirect", permitAll)
authorize(HttpMethod.GET, "/api/images/**", permitAll)
authorize(anyRequest, authenticated)
}
formLogin {
Expand Down

0 comments on commit 75944f4

Please sign in to comment.