diff --git a/src/main/java/com/alist/api/common/modules/file/dto/TusHookEventDto.java b/src/main/java/com/alist/api/common/modules/file/dto/TusHookEventDto.java deleted file mode 100644 index 9770184..0000000 --- a/src/main/java/com/alist/api/common/modules/file/dto/TusHookEventDto.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.alist.api.common.modules.file.dto; - - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import lombok.Getter; -import lombok.Setter; - -@Getter -@Setter -@JsonIgnoreProperties(ignoreUnknown = true) -public class TusHookEventDto { - private String Type; -} diff --git a/src/main/java/com/alist/api/common/modules/file/dto/TusHookUploadDto.java b/src/main/java/com/alist/api/common/modules/file/dto/TusHookUploadDto.java deleted file mode 100644 index f767e38..0000000 --- a/src/main/java/com/alist/api/common/modules/file/dto/TusHookUploadDto.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.alist.api.common.modules.file.dto; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import lombok.Getter; -import lombok.Setter; - -import java.util.Map; - -@Getter -@Setter -@JsonIgnoreProperties(ignoreUnknown = true) -public class TusHookUploadDto { - private String id; - private Long size; - private Map metaData; -} diff --git a/src/main/java/com/alist/api/common/modules/file/service/FileService.java b/src/main/java/com/alist/api/common/modules/file/service/FileService.java index 7c6ccdf..2ecab84 100644 --- a/src/main/java/com/alist/api/common/modules/file/service/FileService.java +++ b/src/main/java/com/alist/api/common/modules/file/service/FileService.java @@ -1,16 +1,20 @@ package com.alist.api.common.modules.file.service; import com.alist.api.common.modules.file.dto.TusHookDto; -import com.alist.api.common.modules.file.dto.TusHookUploadDto; import com.alist.api.common.modules.file.dto.UploadStartDto; import com.alist.api.common.modules.file.dto.UploadTokenDto; import com.alist.api.common.modules.file.mapper.FileMapper; import com.alist.api.config.jwt.JwtTokenProvider; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; -import java.util.Map; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.time.Duration; +import java.util.HexFormat; import java.util.UUID; @Slf4j @@ -22,12 +26,17 @@ public class FileService { @Value("${file.upload.public-base-url}") private String publicBaseUrl; + @Value("${file.upload.auth-cache.ttl-seconds:20}") + private long uploadAuthCacheTtlSeconds; + private final FileMapper fileMapper; private final JwtTokenProvider jwtTokenProvider; + private final StringRedisTemplate stringRedisTemplate; - public FileService(FileMapper fileMapper, JwtTokenProvider jwtTokenProvider) { + public FileService(FileMapper fileMapper, JwtTokenProvider jwtTokenProvider, StringRedisTemplate stringRedisTemplate) { this.fileMapper = fileMapper; this.jwtTokenProvider = jwtTokenProvider; + this.stringRedisTemplate = stringRedisTemplate; } public UploadStartDto insertFileMasterUpload(Integer userIdx, Integer userTokenIdx, UploadStartDto uploadStartDto) { @@ -62,12 +71,35 @@ public class FileService { return uploadTokenDto; } - int isOk = fileMapper.selectFileMasterCountByFileMasterKeyAndDelYnAndStatus(uploadTokenDto); + String cacheKey = buildAuthCacheKey(uploadTokenDto.getFileMasterKey(), uploadTokenDto.getUploadToken()); - if (isOk > 0) { - uploadTokenDto.setResultCode(200); - } else { - uploadTokenDto.setResultCode(403); + try { + String cached = stringRedisTemplate.opsForValue().get(cacheKey); + if (cached != null) { + try { + uploadTokenDto.setResultCode(Integer.parseInt(cached)); + return uploadTokenDto; + } catch (NumberFormatException parseEx) { + log.warn("uploadAuth cache parse fail. key={}, value={}", cacheKey, cached); + stringRedisTemplate.delete(cacheKey); // 오염 캐시 정리 + } + } + } catch (Exception redisGetEx) { + log.warn("uploadAuth cache get fail. fallback DB. key={}", cacheKey, redisGetEx); + } + + int isOk = fileMapper.selectFileMasterCountByFileMasterKeyAndDelYnAndStatus(uploadTokenDto); + int resultCode = (isOk > 0) ? 200 : 403; + uploadTokenDto.setResultCode(resultCode); + + try { + stringRedisTemplate.opsForValue().set( + cacheKey, + String.valueOf(resultCode), + Duration.ofSeconds(safeTtlSeconds()) + ); + } catch (Exception redisSetEx) { + log.warn("uploadAuth cache set fail. key={}, resultCode={}", cacheKey, resultCode, redisSetEx); } return uploadTokenDto; @@ -76,4 +108,35 @@ public class FileService { public void insertFileAndSave(TusHookDto tusHookDto) { } + + private String buildAuthCacheKey(String fileMasterKey, String uploadToken) { + return "upload:alist:auth:" + fileMasterKey + ":" + sha256(uploadToken); + } + + private String sha256(String value) { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hashed = digest.digest(value.getBytes(StandardCharsets.UTF_8)); + return HexFormat.of().formatHex(hashed); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("SHA-256 algorithm not available", e); + } + } + + private void evictUploadAuthCacheByFileMasterKey(String fileMasterKey) { + try { + String pattern = "upload:alist:auth:" + fileMasterKey + ":*"; + var keys = stringRedisTemplate.keys(pattern); + if (!keys.isEmpty()) { + stringRedisTemplate.delete(keys); + } + } catch (Exception e) { + log.warn("uploadAuth cache evict fail. fileMasterKey={}", fileMasterKey, e); + } + } + + private long safeTtlSeconds() { + // 설정 실수 방어: 0/음수면 기본값 20초 사용 + return (uploadAuthCacheTtlSeconds > 0) ? uploadAuthCacheTtlSeconds : 20L; + } } diff --git a/src/main/java/com/alist/api/config/RedisSessionConfig.java b/src/main/java/com/alist/api/config/RedisSessionConfig.java index 97a7d1b..07fd120 100644 --- a/src/main/java/com/alist/api/config/RedisSessionConfig.java +++ b/src/main/java/com/alist/api/config/RedisSessionConfig.java @@ -14,7 +14,7 @@ public class RedisSessionConfig { @Value("${cookie.secure}") private boolean cookieSecure; - @Value("${cookie.domain}") + @Value("${cookie.domain:}") private String cookieDomain; @Value("${cookie.name}") @@ -31,6 +31,10 @@ public class RedisSessionConfig { serializer.setUseSecureCookie(cookieSecure); // 환경별 설정 (local: false, pjt: true) serializer.setUseHttpOnlyCookie(true); // XSS 방지 serializer.setSameSite("Lax"); // CSRF 방지 + + if (cookieDomain != null && !cookieDomain.isBlank()) { + serializer.setDomainName(cookieDomain.trim()); + } return serializer; } diff --git a/src/main/resources/application-local.yaml b/src/main/resources/application-local.yaml index b11ccb0..14806fd 100644 --- a/src/main/resources/application-local.yaml +++ b/src/main/resources/application-local.yaml @@ -33,8 +33,8 @@ jwt: refresh-token-validity-seconds: 2592000 cookie: - secure: true # 로컬 개발 환경 (HTTP) - domain: pjt.kr + secure: false # 로컬 개발 환경 (HTTP) + domain: name: ALIST_SESSION swagger: @@ -46,6 +46,8 @@ file: upload: tus-endpoint: https://file-alist.pjt.kr/tus/files/ public-base-url: https://file-alist.pjt.kr + auth-cache: + ttl-seconds: 20 springdoc: api-docs: diff --git a/src/main/resources/application-pjt.yaml b/src/main/resources/application-pjt.yaml index da59e09..b91e499 100644 --- a/src/main/resources/application-pjt.yaml +++ b/src/main/resources/application-pjt.yaml @@ -48,6 +48,8 @@ file: upload: tus-endpoint: https://file-alist.pjt.kr/tus/files/ public-base-url: https://file-alist.pjt.kr + auth-cache: + ttl-seconds: 20 springdoc: api-docs: