5
Help us understand the problem. What are the problem?

posted at

updated at

Spring Boot でAmazon S3にファイルをアップロードする機能の実装

はじめに

Spring Bootで作成したアプリケーション(ローカル環境)から、Amazon S3に接続して所定のバケットにファイルをアップロードする機能を実装するまでの流れをまとめました。

使用技術

Spring Boot 2.5.6
Spring Security
spring-cloud-starter-aws
Thymeleaf
maven
AWS(IAM,S3)

AWS側の準備

Spring BootからS3へ接続するために、AWSで設定するのは以下2点です。
1.IAMでS3への権限を持ったユーザを作成し、アクセスキーを確認する
2.パブリックアクセスをオフにしたバケットを作成する

IAMでS3への権限を持ったユーザを作成し、アクセスキーを確認する

IAMから「ユーザーを追加」を選択します。
Inkedqiita-aws01._LI.jpg
「アクセスキー・プログラムによるアクセス」にチェックを入れます。

Inkedqiita-aws02_LI.jpg
S3を操作出来る権限を付与します。タグの追加は省略しても大丈夫です。

Inkedqiita-aws03_LI.jpg
作成が完了すると、アクセスキーとシークレットアクセスキーが確認できます。スクショかメモかcsvで保存しましょう。

Inkedqiita-aws04_LI.jpg
シークレットアクセスキーの確認を忘れた場合は、「ユーザー名」をクリック→「認証情報」タブから、アクセスキーを再度生成することが出来ます。

パブリックアクセスをオフにしたバケットを作成する

qiita-aws-s306.jpg
バケットを作成する際は「パブリックアクセスをすべてブロック」のチェックを外して作成します。チェック入れたままだと外部(アプリケーション)からのアップロードが出来ません。

バックエンド

依存関係

以下を追加

pox.xml
        <dependency>
		    <groupId>org.springframework.cloud</groupId>
		    <artifactId>spring-cloud-starter-aws</artifactId>
		    <version>2.2.6.RELEASE</version>
		</dependency>

properties

application.propertiesに以下を追加

application.properties
spring.servlet.multipart.max-request-size=10MB
spring.servlet.multipart.max-file-size=10MB

cloud.aws.credentials.accessKey=
cloud.aws.credentials.secretKey=
cloud.aws.stack.auto=false
cloud.aws.region.auto=false
cloud.aws.region.static=ap-northeast-1

上記ではaccessKeyとsecretKeyに値を記載していませんが、実際にアプリを動かす場合は「AWS側の準備」で確認したアクセスキー、シークレットキーの値を記載してください。

※プロパティファイルにシークレットキーを記載したままGithub等へ連携すると悪用される危険性がありますので、ご注意ください。

Config

S3Config.java
import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;

@Configuration
public class S3Config {
	
	@Value("${cloud.aws.region.static}")
	private String s3ClientRegion;
	
	@Value("${cloud.aws.credentials.access-key}")
	private String accessKey;

	@Value("${cloud.aws.credentials.secret-key}")
	private String secretKey;
	  
	@Bean
	public AmazonS3 s3Client() {
		AWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey);
	    return AmazonS3ClientBuilder.standard()
	    		.withCredentials(new AWSStaticCredentialsProvider(credentials))
	            .withRegion(s3ClientRegion).build();
	    }

}

@Valueでプロパティファイルに記載したアクセスキー等の値を取得します。
Beanに登録しているAmazonS3型ですが、参考文献によると「AmazonS3Webサービスにアクセスするためのインターフェース」とのことです。

Form

Form.java
import org.springframework.web.multipart.MultipartFile;

public class FileUploadForm {
	
    private MultipartFile multipartFile;

    @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss")
	private LocalDateTime createAt;

    ///getter & setter

Service

FileUploadService.java
@Service
public class FileUploadService {
	
	private final AmazonS3 s3Client;
	
	public FileUploadService(AmazonS3 s3Client) {
		this.s3Client = s3Client;
	}
	
    public String fileUpload(FileUploadForm fileUploadForm,String s3PathName) 
			       throws IOException{
		DateTimeFormatter fm = DateTimeFormatter.ofPattern("yyyy-MM-dd HH-mm-ss");
		String extension = FilenameUtils.getExtension(fileUploadForm.getMultipartFile().getOriginalFilename()).toLowerCase();
        String fileName = fileUploadForm.getCreateAt().format(fm) +"." + extension;
        File uploadFile = new File(fileName);
    
        //try-with-resources
        try (FileOutputStream uploadFileStream = new FileOutputStream(uploadFile)){
    		byte[] bytes = fileUploadForm.getMultipartFile().getBytes();
	        uploadFileStream.write(bytes);
        	
        	//引数:S3の格納先オブジェクト名,ファイル名,ファイル
        	s3Client.putObject(s3PathName, fileName, uploadFile);
        	uploadFile.delete();
        	return fileName;
        } catch (AmazonServiceException e) {
        	e.printStackTrace();
        	throw e;
        }  catch (IOException e) {
            e.printStackTrace();
            throw e;
        }
        
	}
}

fileUploadメソッドでは、ファイルを受け取ったFormクラスとS3の格納先名を引数で受け取り、ファイルをアップロードする処理を行っています。
あくまでもサンプルコードなので、画像をアップロードする前の「ファイル形式が正しいか」等のチェックは省略しております。

Controller

DiaryRecordController.java
@Controller
public class DiaryRecordController {
	
	private final DiaryRecordService diaryRecordService;
	
	private final FileUploadService fileUploadService;
	
	public DiaryRecordController(DiaryRecordService diaryRecordService,
			FileUploadService fileUploadService) {
		this.diaryRecordService = diaryRecordService;
		this.fileUploadService = fileUploadService;
	}

    @GetMapping("/index/create")
	public String showCreateContent(@ModelAttribute("diaryRecordForm") DiaryRecordForm form,
			                        @ModelAttribute("fileUploadForm") FileUploadForm file,Model model) {
		return "UserCalendar/Create";
	}

    @PostMapping("/index/create/insert")
	public String createContent(@AuthenticationPrincipal AccountUserDetails details,
			                    @ModelAttribute("diaryRecordForm") @Validated DiaryRecordForm form,BindingResult resultForm,
			                    @ModelAttribute("fileUploadForm")  FileUploadForm file,
			                    Model model) throws Exception{
		if(resultForm.hasErrors()) {
			return "UserCalendar/Create";
		}
		
		String imageName = null;
		LocalDateTime dateTime = LocalDateTime.now();
		//ファイルが空でない場合に、ファイルをアップロードする
		if(!Objects.isNull(file.getMultipartFile())){
			String s3Path = "spring-infra-wp-copy/" + details.getUsername();
			file.setCreateAt(dateTime);
			imageName = fileUploadService.fileUpload(file, s3Path);	
		}
		
		//~~~データベースへレコードをinsertする処理
		
		return "redirect:/index";
	}
}

Controllerクラスでは、ファイルをPOST通信で受け取ってServiceクラスのアップロード処理を呼び出しています。ファイルのバリデーションも必要ですが、今回は省略しています。
fileUploadメソッドの引数に渡すS3の格納先は、「バケット名/ログイン中のユーザ名」としています。

フロントエンド

html

create.html
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>sample</title>
    <meta charset="UTF-8">
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" 
		  integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
    <link rel="stylesheet" th:href="@{/css/style.css}" type="text/css">
</head>

<body>
<div th:replace="flagment/header :: user-header"></div><hr>
<h2>食事記録登録</h2>
<div class="container">
    <form class="row" id="category" enctype="multipart/form-data" th:action="@{/index/create/insert}" method="post" >
    <div class="col-sm-6" th:object="${diaryRecordForm}">
    <!-->省略<-->
	    <div th:object="${fileUploadForm}">
            <input type="file" th:field="*{multipartFile}">
        </div>
	    <button type="submit">登録</button>
	</div>
	</form>
</div>
</body>
</html>

結果・動作確認

qiita-aws-s307.jpg
今回作成したばかりのS3バケットには、ファイルが何も入っていません。

qiita-aws-s308.jpg
ローカルでアプリを起動させ、ファイルを選択して「登録」を押下すると...

qiita-aws-s309.jpg
S3バケットにファイルが格納されました。ファイル名もServiceクラス内で指定した名前になっています。
今回の格納先は「バケット名/ログイン中のユーザ名」としていますが、指定のフォルダ(ユーザ名のフォルダ)が無い場合はフォルダを作って格納されるみたいです。便利~!

終わりに

本来は「画像形式以外は受け付けない」といったバリデーションを設けて不正なファイルを弾く処理が必要ですが、本記事では省略しております。気になった方は調べてみてください。
ファイルアップロード処理のとっかかりになれば幸いです。

参考文献

Register as a new user and use Qiita more conveniently

  1. You can follow users and tags
  2. you can stock useful information
  3. You can make editorial suggestions for articles
What you can do with signing up
5
Help us understand the problem. What are the problem?