이미지 저장에 관한내용
DB저장 -> 2.AWS S3 -> 3.AWS S3 presignedURL
이 순으로 개선해 나갔다
MultipartFile로 받아와서 DB에 저장한다 이때 저장만 구현하고 클라이언트가 다운로드하는건 구현 안했다.
중간에 S3이용한다는 계획을 들었기때문에...!
컨트롤러단
@RequiredArgsConstructor
@RequestMapping("/api/managers/notices")
@RestController
public class NoticeController {
private final NoticeService noticeService;
private final PresignedUrlService presignedUrlService;
private String path; //이미지파일 경로
//관리자 공지사항 등록
@PostMapping("")
public ResponseEntity saveNotice(@RequestBody @Valid NoticeRequest noticeRequest,
@AuthenticationPrincipal UserDetailsImpl userDetails) {
String imageUrl = presignedUrlService.findByName(path);
noticeService.saveNotice(noticeRequest, userDetails.getBase().getId(),imageUrl);
return ResponseEntity.ok("등록 완료");
//new ResponseEntity<>("등록완료",HttpStatus.CREATED);
}
서비스단 UUID 사용해서 중복을 피해줬다.
멀티파트~에 존재하는 메소드인
file.transferTo ()
이메소드를 이용해 파일경로,파일네임을 저장해주었다
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class T_exerciseServiceImple implements T_exerciseService{
private final T_exerciseRepository t_exerciseRepository;
/**
* 오운완 게시물 생성
* @param title 제목
* @param content 내용
* @param file 이게 올릴 이미지임..!
* @param user 관계를 맺기 위해 ~ 인증된 객체 꺼내옴
* @return http status
* @throws NullPointerException ?
* @throws IOException ?
*/
@Transactional
@Override
public ResponseEntity<String> creatTExerciseBord(String title, String content, MultipartFile file, User user) throws NullPointerException, IOException {
UUID uuid = UUID.randomUUID();
String filename = uuid+"_"+file.getOriginalFilename();
String filepath = System.getProperty("user.dir")+"/src/main/resources/static/files";
File savefile = new File(filepath, filename);
file.transferTo(savefile);
T_exercise t_exercise = new T_exercise(title,content,filename,filepath,user);
t_exerciseRepository.save(t_exercise);
return new ResponseEntity<>("등록완료", HttpStatus.OK);
}
}
여기서 S3에다가 업로드 다운로드 한다는 계획을 듣고 SHARE 폴더에다가 aws_s3관련 로직들을 추가해줬다
일단 어플리케이션 프로펄티스를 yml로 통합시켜줫다 aws ec2 대비해서
이때 yml파일을 따로 분리해서 깃.이그노어에 등록해놨다면 나중에 관리하기가 더 편했을 듯하다..!
s3 설정파일이다 ..
yml에 설정한 값을 가져온다 ! ~
@Configuration
public class S3Config{
//@Value("${cloud.aws.credentials.access-key}")
@Value("${cloud.aws.credentials.access-key}")
private String accessKey;
@Value("${cloud.aws.credentials.secret-key}")
private String secretKey;
@Value("${cloud.aws.region.static}")
private String region;
@Bean
@Primary
public BasicAWSCredentials awsCredentialsProvider(){
return new BasicAWSCredentials(accessKey, secretKey);
}
@Bean
public AmazonS3 amazonS3(){
return AmazonS3ClientBuilder.standard()
.withRegion(region)
.withCredentials(new AWSStaticCredentialsProvider(awsCredentialsProvider()))
.build();
}
}
S3에 이미지 업로드를 담당하는 클래스다.
objectMetadata를 사용했다
이방법을 사용하면 로컬에 이미지를 저장하지 않고 S3 로 업로드를 할 수 있다
https://galid1.tistory.com/591
AWS SDK - JAVA를 이용해 S3에 파일 업로드시 로컬에 저장되지 않도록 하기
이번 포스팅에는 사용자의 업로드 요청시 로컬 스토리지가 아닌 AWS의 S3에 저장하는데, 로컬에는 파일이 저장되지 않은 채로 바로 업로드 하는 방법을 알아보도록 하겠습니다 이번 포스팅에는
galid1.tistory.com
db 에는 이미지 경로만 가지고 있으면 된다
@RequiredArgsConstructor
@Component
public class S3Uploader {
private final AmazonS3 amazonS3Client;
//@Value("${cloud.aws.s3.bucket}")
@Value("${cloud.aws.s3.bucket}")
public String bucket;
public String uploadOne(MultipartFile multipartFile,String dir) throws IOException{
String imgPath;
String fileName = creatFileName()+multipartFile.getName();
ObjectMetadata objectMetadata = new ObjectMetadata();
objectMetadata.setContentLength(multipartFile.getSize());
objectMetadata.setContentType(multipartFile.getContentType());
try(InputStream inputStream = multipartFile.getInputStream()) {
amazonS3Client.putObject(
new PutObjectRequest(bucket+dir,fileName,inputStream,objectMetadata)
.withCannedAcl(CannedAccessControlList.PublicRead));
imgPath = amazonS3Client.getUrl(bucket+dir,fileName).toString();
}catch (IOException e){
throw new IOException();
}
return imgPath;
}
public void deleteFile(String imagePathLong){
try {
String imagePath = imagePathLong.substring(54);
amazonS3Client.deleteObject(
new DeleteObjectRequest(bucket,imagePath));
}catch (
AmazonS3Exception e){
e.printStackTrace();
}
}
public String creatFileName() throws IOException{
return UUID.randomUUID().toString();
}
}
S3사용에서 좀더 개선 시킬 방법이 있었다
일단 S3 까지 오게된 ~ 흐름
클라-백엔드서버-S3서버의 전송경로를
클라-S3서버 이렇게 전송경로를 간략하게 할 수있고 여기에 허가된 사람만 업로드및 다운로드를 해주게 하기 위해서 presigedURL을 사용하는것..!
그러면 어떻게 해야하냐...
일단 내가한방법이다 무조건 이미지가 업로드 되야하는 게시판을 만들었으니 좀 더 간결하다(오운완 인증)
이미지 업로드기준 ..!
1.글작성버튼 클릭시..
2.백엔드 서버에 presigendURL 호출
3.백엔드 서버는 토큰을 검사하고 유효한 토큰이라면~ presignedURL S3서버로부터 요청해서 다시 클라
쪽에 설정한 유효기간동안 유효한 presigendURL을 넘겨줌
4.클라이언트는 presigendURL을 성공적으로 받았으면 두가지 요청을 수행함
4-1)발급받은 presigned URL을 통해 S3서버로 이미지 업로드
4-2) 게시글 제목과 내용을 백엔드 서버로 전송 저장
5.백엔드 서버는 제목과 내용을 받고 저장경로를 받아와서 같이 저장해준다
이렇게 5가지 단계를 거쳐서 이미지 업로드에 한에 presignedURL을 발급받아 업로드 했다
당연히 이미지 데이터가 백엔드 서버로 전송되지 않으니 부담이 줄어들 것이다.
프리사인 유알엘 발급 [컨트롤러]
path로 저장경로를 지정해준다
@PostMapping("/presigned")
public String creatPresigned(@RequestBody ImageNameDTO imageNameDTO,
@AuthenticationPrincipal UserDetailsImpl userDetails) {
BaseEntity base = userDetails.getBase();
boolean checkUser = baseService.checkUser(base.getUsername());
if(checkUser){
path ="texe";
String imageName = imageNameDTO.getImageName();
return presignedUrlService.getPreSignedUrl(path,imageName);
}else {
throw new CustomException(ExceptionStatus.WRONG_USERNAME);
}
}
프리사인URL [서비스단]
프리사인 URL 유효기간 설정및 uuid이용 동일 이미지이름이라도 겹치지 않게 처리해줬다
@Slf4j
@Service
@RequiredArgsConstructor
public class PresignedUrlService {
private final AmazonS3 amazonS3;
private String useOnlyOneFileName;
@Value("${cloud.aws.s3.bucket}")
private String bucket;
@Value("${cloud.aws.region.static}")
private String location;
public String getPreSignedUrl(String prefix, String fileName) {
String onlyOneFileName = onlyOneFileName(fileName);
useOnlyOneFileName = onlyOneFileName;
if (!prefix.equals("")) {
onlyOneFileName = prefix + "/" + onlyOneFileName;
}
GeneratePresignedUrlRequest generatePresignedUrlRequest = getGeneratePreSignedUrlRequest(bucket, onlyOneFileName);
return amazonS3.generatePresignedUrl(generatePresignedUrlRequest).toString();
}
private GeneratePresignedUrlRequest getGeneratePreSignedUrlRequest(String bucket, String fileName) {
GeneratePresignedUrlRequest generatePresignedUrlRequest =
new GeneratePresignedUrlRequest(bucket, fileName)
.withMethod(HttpMethod.PUT)
.withExpiration(getPreSignedUrlExpiration());
generatePresignedUrlRequest.addRequestParameter(
Headers.S3_CANNED_ACL,
CannedAccessControlList.PublicRead.toString());
return generatePresignedUrlRequest;
}
private Date getPreSignedUrlExpiration() {
Date expiration = new Date();
long expTimeMillis = expiration.getTime();
expTimeMillis += 1000 * 60 * 2;
expiration.setTime(expTimeMillis);
log.info(expiration.toString());
return expiration;
}
private String onlyOneFileName(String filename){
return UUID.randomUUID().toString()+filename;
}
public String findByName(String path) {
if (!amazonS3.doesObjectExist(bucket, useOnlyOneFileName))
return "File does not exist";
log.info("Generating signed URL for file name {}", useOnlyOneFileName);
String editPath = "/"+path+"/";
return amazonS3.getUrl(bucket+ editPath,useOnlyOneFileName).toString();
// return "https://"+bucket+".s3."+location+".amazonaws.com/"+path+"/"+useOnlyOneFileName;
}
}
아직 미완성인 부분이있다
지금은 업로드 즉 S3 put요청에 대해서만 presigned URL 적용시켰다
그러면 문제가 되는게 버킷을 프라이빗하게 관리하지 못한다 즉 아직 퍼블릭하게 열려있다는 점...
현재 put요청이랑 get요청 버킷권한으로 막아놨지만
이건 내가 get요청에 대해 presigned URL을 아직 적용하지 못했기때문이다..
1.첫번째 S3 하나의 버켓에 정적웹 호스팅용도와 이미지저장소 사용함..
1-1) 정적웹 호스팅 버킷을 퍼블릭하게 오픈해야함...
1-2) 아 그러면 이미지는 어떻게해야하지..? -> 버켓을 분리해서 용도에 맞게 관리해야하네.!!
처음에는 put get요청 둘다 퍼블릭하게 열어놨는데...?
버킷 주소가 노출되면 presigend URL 없어도 put이 되어버리네..!
그러면 버킷정책 변경해서 ..put get 요청을 막아놓자...-> OK put 막아놨다 ..!
3..아 그러면 get요청마다 presigned URL 발급받아서 해야하는데 ..프로젝트 마감이 촉박하다...
3-1) 일단 중요한건 다 막아놨으니... 버킷이 퍼블릭이라는 점 빼고는
3-2) 그럼 get요청에 대한건...계속 일정 시간마다 ,,,요청해야하나..
3-2-1) 아... AWS 클라우드 프론트 이용해서 캐싱기능까지 적용한다면 S3도 백엔드서버도 부담이 적어지고 할것..!