T7_0609
這次依然比較少: 做的是圖片儲存的部分 在登入完後,需要使用使用者本身的cookie來進行將圖片儲存至firebase storage
Spring Boot Meets Firebase: My Journey of Building a File Upload System🚀
FileStorageController
@RestController
@RequestMapping("/api/files")
public class FileStorageController {
@Autowired
private FileStorageService fileStorageService;
@PostMapping("/upload")
public ResponseEntity<String> uploadFile(@RequestParam MultipartFile file,
@CookieValue(value = "id_token", required = false) String idTokenCookie) {
CookieAuthUtil.verifyIdToken(idTokenCookie);
try {
// 3. 驗證並解碼 ID Token
FirebaseToken decoded = FirebaseAuth.getInstance()
.verifyIdToken(idTokenCookie);
String uid = decoded.getUid();
// String email = decoded.getEmail();
// String name = decoded.getName();
// String head = decoded.getPicture();
String fileId = fileStorageService.uploadFile(file, uid);
String successJson = "{\"fileId\":\"" + fileId + "\"}";
return ResponseEntity.ok(successJson);
} catch (Exception e) {
// 4. 回 500 並回錯誤訊息
// 先把雙引號 escape,避免破壞 JSON
String escaped = e.getMessage() == null
? ""
: e.getMessage().replace("\"", "\\\"");
String errorJson = "{\"error\":\"" + escaped + "\"}";
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(errorJson);
}
}
@GetMapping("/retrieve")
public ResponseEntity<List<FileResponse>> retrieveSpecificFile(@CookieValue(value = "id_token", required = false) String idTokenCookie) {
try {
// 1. 驗證並解析 token
FirebaseToken decoded = CookieAuthUtil.verifyIdToken(idTokenCookie);
String uid = decoded.getUid();
// 2. 查出所有該 user 的檔案
List<FileMetaData> files = fileStorageService.getFilesByUserId(decoded.getUid());
// 3. 用迴圈一張一張讀圖,放到新的 List 裡
List<FileResponse> images = new ArrayList<>();
for(FileMetaData file : files) {
// file.getUniqueId() 就是你 retrieveFile() 要傳的 fileId
FileResponse resp = fileStorageService.retrieveFile(file.getUniqueId());
images.add(resp);
}
return ResponseEntity.ok(images);
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(null);
}
}
@GetMapping("/retrieve/{fileId}")
public ResponseEntity<FileResponse> retrieveFile(@PathVariable String fileId,
@CookieValue(value = "id_token", required = false) String idTokenCookie) {
try {
CookieAuthUtil.verifyIdToken(idTokenCookie);
FileResponse fileResponse = fileStorageService.retrieveFile(fileId);
return ResponseEntity.ok(fileResponse);
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(null);
}
}
}
CookieAuthUtil
/**
* 一個靜態工具類,用來驗證 id_token cookie。
*/
public class CookieAuthUtil {
/**
* 驗證傳入的 idToken(從 Cookie 拿到的值),
* 驗證成功回傳解碼後的 FirebaseToken,
* 失敗則丟 IllegalArgumentException。
*
* @param idTokenCookie 從 @CookieValue("id_token") 拿到的字串
* @return FirebaseToken 已驗證的 token
* @throws IllegalArgumentException 若 token 為空或驗證失敗
*/
public static FirebaseToken verifyIdToken(String idTokenCookie) {
if (!StringUtils.hasText(idTokenCookie)) {
throw new IllegalArgumentException("Missing ID token");
}
try {
return FirebaseAuth.getInstance().verifyIdToken(idTokenCookie);
} catch (FirebaseAuthException e) {
throw new IllegalArgumentException("Invalid ID token", e);
}
}
/**
* 簡單檢查 token 是否有效,回傳 boolean。
*
* @param idTokenCookie 從 Cookie 拿到的字串
* @return true = token 有效;false = token 為空或驗證失敗
*/
public static boolean isTokenValid(String idTokenCookie) {
try {
verifyIdToken(idTokenCookie);
return true;
} catch (IllegalArgumentException e) {
return false;
}
}
}
FileStorageService
@Service
public class FileStorageService {
private final Storage storage;
private final FileMetaDataRepository repo;
private final String bucketName = "test-2ea39.firebasestorage.app";
public FileStorageService(FileMetaDataRepository repo) throws IOException {
this.repo = repo;
ClassPathResource resource = new ClassPathResource("testAccountKey.json");
InputStream serviceAccount = resource.getInputStream();
GoogleCredentials credentials = GoogleCredentials.fromStream(serviceAccount)
.createScoped(Lists.newArrayList("https://www.googleapis.com/auth/cloud-platform"));
this.storage = StorageOptions.newBuilder().setCredentials(credentials).build().getService();
}
/**
* 拿到某使用者所有上傳檔案的 metadata list
*/
public List<FileMetaData> getFilesByUserId(String userId) {
return repo.findAllByUserId(userId);
}
/**
* 拿單筆
*/
public FileMetaData getByUniqueId(String uniqueId) {
return repo.findByUniqueId(uniqueId);
}
public String uploadFile(MultipartFile file, String _uid) throws IOException {
if (file.isEmpty()) {
throw new IllegalArgumentException("File is empty. Please upload a valid file.");
}
String uniqueID = UUID.randomUUID().toString();
String objectName = uniqueID + "_" + file.getOriginalFilename();
BlobId blobId = BlobId.of(bucketName, objectName);
BlobInfo blobInfo = BlobInfo.newBuilder(blobId).build();
storage.create(blobInfo, file.getBytes());
FileMetaData metaData = new FileMetaData();
metaData.setUserId(_uid);
metaData.setUniqueId(uniqueID);
metaData.setObjectName(objectName);
metaData.setUploadDate(LocalDateTime.now());
repo.save(metaData);
return uniqueID;
}
public FileResponse retrieveFile(String fileId) {
FileMetaData fileMetadata = repo.findByUniqueId(fileId);
if (fileMetadata == null) {
throw new IllegalArgumentException("No file found with the given ID: " + fileId);
}
String objectName = fileMetadata.getObjectName();
BlobId blobId = BlobId.of(bucketName, objectName);
Blob blob = storage.get(blobId);
if (blob == null || !blob.exists()) {
throw new IllegalArgumentException("No file found with the given ID: " + fileId);
}
FileResponse fileResponse = new FileResponse(objectName, blob.getContent());
return fileResponse;
}
}
檔案上傳的範例

const handleupload = async () => {
if (!imageUrl) {
alert("請先上傳圖片");
return;
}
const input = document.getElementById("fileInput");
const file = input.files[0];
// 1. 建立 FormData 並 append 檔案
const formData = new FormData();
formData.append("file", file); // key 要跟 @RequestParam("file") 一致
const response = await fetch("http://localhost:8080/api/files/upload", {
method: "POST", // 請求方法:使用 POST,表示要向伺服器送出資料
mode: "cors", // CORS 模式——開啟跨域請求,允許向不同網域/埠號/協議的伺服器發出請求
credentials: "include", // 攜帶憑證——一定要加,當且僅當你需要和伺服器共享 Cookie、HTTP 認證資訊時使用
body: formData, // 請求主體:把前面用 new FormData() 建構好的表單資料(含檔案或欄位)當作請求內容送出
});
// 這裡可以加入 AI 修圖的邏輯
// 3. 讀結果
if (!response.ok) {
console.error("上傳失敗", await response.text());
} else {
console.log("上傳成功", await response.json());
}
};
點擊畫面的上傳
將圖片上傳後
點擊AI修圖(暫時設置為檔案上傳)
會在Console輸出上上傳成功,還有圖片儲存至firebase storage後,會產生objectName儲存進資料庫中,用作查詢圖片使用

在歷史圖片中,我是暫時將其設置為圖片儲存區
再開啟歷史圖片(圖片儲存區)
會先將cookie送至後端,後端會去firebase Authencation抓取user_id
再透過user_id去資料庫抓取objectName至firebase storage得取圖片顯示於畫面上
const fetchImages = async () => {
try {
const response = await fetch(
"http://localhost:8080/api/files/retrieve",
{
method: "GET", // 請求方法:使用 GET,表示要向伺服器請求資料
mode: "cors", // CORS 模式——開啟跨域請求,允許向不同網域/埠號/協議的伺服器發出請求
credentials: "include", // 攜帶憑證——一定要加,當且僅當你需要和伺服器共享 Cookie、HTTP 認證資訊時使用
}
);
// 後端傳回 [{ fileName, fileContent(Base64) }, ...]
const data = await response.json();
// 2️⃣ 把每筆 Base64 轉成可用的 data URL
const imgs = data.map((file) => {
// 根據副檔名判斷 MIME type
const ext = file.fileName.split(".").pop().toLowerCase();
const mime = ext === "png" ? "image/png" : "image/jpeg";
return {
name: file.fileName,
src: `data:${mime};base64,${file.fileContent}`,
};
});
if (!response.ok) {
console.error("上傳失敗");
} else {
console.log("上傳成功");
}
setImages(imgs);
} catch (error) {
console.error(error);
}
};