diff --git a/Server/src/main/java/JGS/CasperEvent/domain/event/entity/participants/LotteryParticipants.java b/Server/src/main/java/JGS/CasperEvent/domain/event/entity/participants/LotteryParticipants.java index 387084a1..39037862 100644 --- a/Server/src/main/java/JGS/CasperEvent/domain/event/entity/participants/LotteryParticipants.java +++ b/Server/src/main/java/JGS/CasperEvent/domain/event/entity/participants/LotteryParticipants.java @@ -12,7 +12,7 @@ public class LotteryParticipants { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @OneToOne + @OneToOne // mappedBy 이용하면 둘 다 저장 안해도 됨 @JoinColumn(name = "base_user_id") //todo: 왜이런지 알아보기 @JsonBackReference diff --git a/Server/src/main/java/JGS/CasperEvent/domain/url/controller/UrlController.java b/Server/src/main/java/JGS/CasperEvent/domain/url/controller/UrlController.java index cc8a266c..3503e936 100644 --- a/Server/src/main/java/JGS/CasperEvent/domain/url/controller/UrlController.java +++ b/Server/src/main/java/JGS/CasperEvent/domain/url/controller/UrlController.java @@ -1,4 +1,44 @@ package JGS.CasperEvent.domain.url.controller; +import JGS.CasperEvent.domain.url.dto.ShortenUrlResponseDto; +import JGS.CasperEvent.domain.url.service.UrlService; +import JGS.CasperEvent.global.entity.BaseUser; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import javax.crypto.BadPaddingException; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; + +@RestController +@RequestMapping("link") public class UrlController { + + private final UrlService urlService; + + @Autowired + public UrlController(UrlService urlService) { + this.urlService = urlService; + } + + @PostMapping + public ResponseEntity generateShortUrl(HttpServletRequest request) throws NoSuchPaddingException, IllegalBlockSizeException, NoSuchAlgorithmException, BadPaddingException, InvalidKeyException { + BaseUser user = (BaseUser) request.getAttribute("user"); + return ResponseEntity + .status(HttpStatus.CREATED) + .body(urlService.generateShortUrl(user)); + } + + @GetMapping("/{encodedId}") + public ResponseEntity redirectOriginalUrl(@PathVariable String encodedId){ + return ResponseEntity + .status(HttpStatus.FOUND) + .header("Location", urlService.getOriginalUrl(encodedId)) + .build(); + } } diff --git a/Server/src/main/java/JGS/CasperEvent/domain/url/dto/ShortenUrlResponseDto.java b/Server/src/main/java/JGS/CasperEvent/domain/url/dto/ShortenUrlResponseDto.java new file mode 100644 index 00000000..84f2ff92 --- /dev/null +++ b/Server/src/main/java/JGS/CasperEvent/domain/url/dto/ShortenUrlResponseDto.java @@ -0,0 +1,4 @@ +package JGS.CasperEvent.domain.url.dto; + +public record ShortenUrlResponseDto(String shortenUrl, String shortenLocalUrl) { +} diff --git a/Server/src/main/java/JGS/CasperEvent/domain/url/entity/OriginalUrl.java b/Server/src/main/java/JGS/CasperEvent/domain/url/entity/OriginalUrl.java deleted file mode 100644 index 0545a31d..00000000 --- a/Server/src/main/java/JGS/CasperEvent/domain/url/entity/OriginalUrl.java +++ /dev/null @@ -1,14 +0,0 @@ -package JGS.CasperEvent.domain.url.entity; - -import JGS.CasperEvent.global.entity.BaseEntity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; - -public class OriginalUrl extends BaseEntity { - @Id - @GeneratedValue(strategy = GenerationType.UUID) - private Long id; - - private String originalUrl; -} diff --git a/Server/src/main/java/JGS/CasperEvent/domain/url/entity/Url.java b/Server/src/main/java/JGS/CasperEvent/domain/url/entity/Url.java new file mode 100644 index 00000000..59b5514c --- /dev/null +++ b/Server/src/main/java/JGS/CasperEvent/domain/url/entity/Url.java @@ -0,0 +1,26 @@ +package JGS.CasperEvent.domain.url.entity; + +import JGS.CasperEvent.global.entity.BaseEntity; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.Getter; + +@Entity +@Getter +public class Url extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String originalUrl; + + public Url(String originalUrl){ + this.originalUrl = originalUrl; + } + + public Url() { + + } +} diff --git a/Server/src/main/java/JGS/CasperEvent/domain/url/repository/UrlRepository.java b/Server/src/main/java/JGS/CasperEvent/domain/url/repository/UrlRepository.java index 202cfb59..f7d8649b 100644 --- a/Server/src/main/java/JGS/CasperEvent/domain/url/repository/UrlRepository.java +++ b/Server/src/main/java/JGS/CasperEvent/domain/url/repository/UrlRepository.java @@ -1,4 +1,12 @@ package JGS.CasperEvent.domain.url.repository; -public interface UrlRepository { +import JGS.CasperEvent.domain.url.entity.Url; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface UrlRepository extends JpaRepository { + Optional findByOriginalUrl(String originalUrl); } diff --git a/Server/src/main/java/JGS/CasperEvent/domain/url/service/UrlService.java b/Server/src/main/java/JGS/CasperEvent/domain/url/service/UrlService.java index df6b852f..dcb53cfa 100644 --- a/Server/src/main/java/JGS/CasperEvent/domain/url/service/UrlService.java +++ b/Server/src/main/java/JGS/CasperEvent/domain/url/service/UrlService.java @@ -1,4 +1,71 @@ package JGS.CasperEvent.domain.url.service; +import JGS.CasperEvent.domain.url.dto.ShortenUrlResponseDto; +import JGS.CasperEvent.domain.url.entity.Url; +import JGS.CasperEvent.domain.url.repository.UrlRepository; +import JGS.CasperEvent.global.entity.BaseUser; +import JGS.CasperEvent.global.util.AESUtils; +import JGS.CasperEvent.global.util.Base62Utils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import javax.crypto.BadPaddingException; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.NoSuchElementException; + +@Service public class UrlService { + + @Value("${client.url}") + private String clientUrl; + @Value("${client.localUrl}") + private String localClientUrl; + + @Value("${shortenUrlService.url}") + private String shortenBaseUrl; + + + private final UrlRepository urlRepository; + private final SecretKey secretKey; + + @Autowired + public UrlService(UrlRepository urlRepository, SecretKey secretKey) { + this.urlRepository = urlRepository; + this.secretKey = secretKey; + } + + //todo: 테스트 끝나면 수정필요 + public ShortenUrlResponseDto generateShortUrl(BaseUser user) throws NoSuchPaddingException, IllegalBlockSizeException, NoSuchAlgorithmException, BadPaddingException, InvalidKeyException { + String encryptedUserId = AESUtils.encrypt(user.getId(), secretKey); + + String originalUrl = clientUrl + "?" + "referralId=" + encryptedUserId; + String originalLocalUrl = localClientUrl + "?" + "referralId=" + encryptedUserId; + + Url url = urlRepository.findByOriginalUrl(originalUrl).orElseGet( + () -> urlRepository.save(new Url(originalUrl)) + ); + Url localUrl = urlRepository.findByOriginalUrl(originalLocalUrl).orElseGet( + () -> urlRepository.save(new Url(originalLocalUrl)) + ); + + Long urlId = url.getId(); + Long localUrlId = localUrl.getId(); + + String shortenUrl = shortenBaseUrl + "/link/" + Base62Utils.encode(urlId); + String shortenLocalUrl = shortenBaseUrl + "/link/" + Base62Utils.encode(localUrlId); + + return new ShortenUrlResponseDto(shortenUrl, shortenLocalUrl); + } + + public String getOriginalUrl(String encodedId){ + Long urlId = Base62Utils.decode(encodedId); + Url url = urlRepository.findById(urlId).orElseThrow(NoSuchElementException::new); + return url.getOriginalUrl(); + } + } diff --git a/Server/src/main/java/JGS/CasperEvent/global/config/SecurityConfig.java b/Server/src/main/java/JGS/CasperEvent/global/config/SecurityConfig.java new file mode 100644 index 00000000..90dbf9ad --- /dev/null +++ b/Server/src/main/java/JGS/CasperEvent/global/config/SecurityConfig.java @@ -0,0 +1,20 @@ +package JGS.CasperEvent.global.config; + +import JGS.CasperEvent.global.util.AESUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import javax.crypto.SecretKey; + +@Configuration +public class SecurityConfig { + + @Value("${spring.encryption.key}") + private String encryptionKey; + + @Bean + public SecretKey secretKey() { + return AESUtils.stringToKey(encryptionKey); + } +} diff --git a/Server/src/main/java/JGS/CasperEvent/global/error/GlobalExceptionHandler.java b/Server/src/main/java/JGS/CasperEvent/global/error/GlobalExceptionHandler.java index 692154a2..de569e89 100644 --- a/Server/src/main/java/JGS/CasperEvent/global/error/GlobalExceptionHandler.java +++ b/Server/src/main/java/JGS/CasperEvent/global/error/GlobalExceptionHandler.java @@ -19,8 +19,8 @@ public class GlobalExceptionHandler { @ExceptionHandler(CustomException.class) public ResponseEntity handler(CustomException e){ return ResponseEntity - .status(HttpStatus.BAD_REQUEST) - .body(ErrorResponse.of(CustomErrorCode.BAD_REQUEST, e.getMessage())); + .status(HttpStatus.valueOf(e.getErrorCode().getStatus())) + .body(ErrorResponse.of(e.getErrorCode(), e.getMessage())); } @ExceptionHandler(MissingRequestCookieException.class) diff --git a/Server/src/main/java/JGS/CasperEvent/global/jwt/filter/JwtAuthorizationFilter.java b/Server/src/main/java/JGS/CasperEvent/global/jwt/filter/JwtAuthorizationFilter.java index cb83d2d8..60c276ba 100644 --- a/Server/src/main/java/JGS/CasperEvent/global/jwt/filter/JwtAuthorizationFilter.java +++ b/Server/src/main/java/JGS/CasperEvent/global/jwt/filter/JwtAuthorizationFilter.java @@ -32,7 +32,7 @@ public class JwtAuthorizationFilter implements Filter { "/event/rush", "/event/lottery/caspers", "/admin/join", "/admin/auth", "/h2", "/h2/*", "/swagger-ui/*", "/v3/api-docs", "/v3/api-docs/*", - "/event/lottery" + "/event/lottery", "/link/*" }; private final String[] blackListUris = new String[]{ "/event/rush/*", "/event/lottery/casperBot" diff --git a/Server/src/main/java/JGS/CasperEvent/global/util/AESUtils.java b/Server/src/main/java/JGS/CasperEvent/global/util/AESUtils.java new file mode 100644 index 00000000..1fc26b96 --- /dev/null +++ b/Server/src/main/java/JGS/CasperEvent/global/util/AESUtils.java @@ -0,0 +1,31 @@ +package JGS.CasperEvent.global.util; + +import javax.crypto.*; +import javax.crypto.spec.SecretKeySpec; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; + +public class AESUtils { + + public static SecretKey stringToKey(String keyString) { + byte[] decodedKey = keyString.getBytes(); + return new SecretKeySpec(decodedKey, 0, decodedKey.length, "AES"); + } + + public static String encrypt(String plainText, SecretKey key) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException { + Cipher cipher = Cipher.getInstance("AES"); + cipher.init(Cipher.ENCRYPT_MODE, key); + byte[] encryptedBytes = cipher.doFinal(plainText.getBytes()); + return Base64.getEncoder().encodeToString(encryptedBytes); + } + + public static String decrypt(String encryptedText, SecretKey key) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException { + Cipher cipher = Cipher.getInstance("AES"); + cipher.init(Cipher.DECRYPT_MODE, key); + byte[] decryptedBytes = cipher.doFinal(Base64.getDecoder().decode(encryptedText)); + return new String(decryptedBytes); + } + + +} diff --git a/Server/src/main/java/JGS/CasperEvent/global/util/Base62Utils.java b/Server/src/main/java/JGS/CasperEvent/global/util/Base62Utils.java new file mode 100644 index 00000000..bd63e1d3 --- /dev/null +++ b/Server/src/main/java/JGS/CasperEvent/global/util/Base62Utils.java @@ -0,0 +1,25 @@ +package JGS.CasperEvent.global.util; + +public class Base62Utils { + private static final String BASE62_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + + public static String encode(long number) { + if (number == 0) return Character.toString(BASE62_CHARS.charAt(0)); + + StringBuilder sb = new StringBuilder(); + while (number > 0) { + int reminder = (int) (number % 62); + sb.append(BASE62_CHARS.charAt(reminder)); + number /= 62; + } + return sb.reverse().toString(); + } + + public static long decode(String str){ + long result = 0; + for (int i = 0; i < str.length(); i++) { + result = result * 62 + BASE62_CHARS.indexOf(str.charAt(i)); + } + return result; + } +} diff --git a/Server/src/main/java/JGS/CasperEvent/global/util/GsonUtil.java b/Server/src/main/java/JGS/CasperEvent/global/util/GsonUtil.java deleted file mode 100644 index 7f6024f5..00000000 --- a/Server/src/main/java/JGS/CasperEvent/global/util/GsonUtil.java +++ /dev/null @@ -1,80 +0,0 @@ -package JGS.CasperEvent.global.util; - -import com.google.gson.*; -import com.google.gson.stream.JsonReader; -import com.google.gson.stream.JsonWriter; - -import java.io.IOException; -import java.lang.reflect.Type; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.LocalTime; -import java.time.format.DateTimeFormatter; - -public class GsonUtil { - private static final String PATTERN_DATE = "yyyy-MM-dd"; - private static final String PATTERN_TIME = "HH:mm:ss"; - private static final String PATTERN_DATETIME = String.format("%s %s", PATTERN_DATE, PATTERN_TIME); - - static class LocalDataTimeAdapter extends TypeAdapter { - DateTimeFormatter format = DateTimeFormatter.ofPattern(PATTERN_DATETIME); - - @Override - public void write(JsonWriter out, LocalDateTime value) throws IOException { - if (value != null) out.value(value.format(format)); - } - - @Override - public LocalDateTime read(JsonReader in) throws IOException { - return LocalDateTime.parse(in.nextString(), format); - } - } - - static class LocalDateAdapter extends TypeAdapter { - DateTimeFormatter format = DateTimeFormatter.ofPattern(PATTERN_DATE); - - @Override - public void write(JsonWriter out, LocalDate value) throws IOException { - out.value(value.format(format)); - } - - @Override - public LocalDate read(JsonReader in) throws IOException { - return LocalDate.parse(in.nextString(), format); - } - } - - static class LocalTimeAdapter extends TypeAdapter { - DateTimeFormatter format = DateTimeFormatter.ofPattern(PATTERN_TIME); - - @Override - public void write(JsonWriter out, LocalTime value) throws IOException { - out.value(value.format(format)); - } - - @Override - public LocalTime read(JsonReader in) throws IOException { - return LocalTime.parse(in.nextString(), format); - } - } - - static class EnumSerializer implements JsonSerializer>{ - @Override - public JsonElement serialize(Enum src, Type typeOfSrc, JsonSerializationContext context) { - return new JsonPrimitive(src.name()); - } - } - - private static final Gson gson = new GsonBuilder() - .disableHtmlEscaping() - .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) - .setDateFormat(PATTERN_DATETIME) - .registerTypeAdapter(LocalDateTime.class, new LocalDataTimeAdapter().nullSafe()) - .registerTypeAdapter(LocalDate.class, new LocalDateAdapter().nullSafe()) - .registerTypeAdapter(LocalTime.class, new LocalTimeAdapter().nullSafe()) - .create(); - - public static Gson getGson(){ - return gson; - } -} diff --git a/Server/src/main/java/JGS/CasperEvent/global/util/UserUtil.java b/Server/src/main/java/JGS/CasperEvent/global/util/UserUtil.java index 711708d1..a3ffb0cb 100644 --- a/Server/src/main/java/JGS/CasperEvent/global/util/UserUtil.java +++ b/Server/src/main/java/JGS/CasperEvent/global/util/UserUtil.java @@ -5,17 +5,6 @@ public class UserUtil { //TODO: 스프링 서버 뻗으면 캐스퍼 아이디 0부터 다시 시작함 private static final AtomicLong counter = new AtomicLong(0); - - //TODO: 현재는 그냥 userData 즉시 반환, 키 이용한 복호화로 수정하기 - public static String getDecodedPhoneNumber(String userData) { - return userData; - } - - //TODO: 현재는 true 리턴, jwt로 변경 필요 - public static Boolean isValidAdminToken(String token){ - if (token.equals("adminToken")) return true; - return false; - } public static long generateId(){ return counter.incrementAndGet(); } diff --git a/Server/src/main/resources/application-prod.yml b/Server/src/main/resources/application-prod.yml index 658d7e19..65bd582a 100644 --- a/Server/src/main/resources/application-prod.yml +++ b/Server/src/main/resources/application-prod.yml @@ -18,4 +18,12 @@ spring: data: redis: host: ${SPRING_REDIS_HOST} - port: ${SPRING_REDIS_PORT} \ No newline at end of file + port: ${SPRING_REDIS_PORT} + encryption: + key: ${AES_128_ENCRYPTION_KEY} + +client: + url: ${CLIENT_URL} + localUrl: ${LOCAL_CLIENT_URL} +shortenUrlService: + url: ${SPRING_SERVER_URL} \ No newline at end of file