diff --git a/src/main/java/com/ajou/hertz/common/auth/controller/AuthController.java b/src/main/java/com/ajou/hertz/common/auth/controller/AuthController.java index 8b9de82..987121d 100644 --- a/src/main/java/com/ajou/hertz/common/auth/controller/AuthController.java +++ b/src/main/java/com/ajou/hertz/common/auth/controller/AuthController.java @@ -2,7 +2,6 @@ import static com.ajou.hertz.common.constant.GlobalConstants.*; -import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -12,8 +11,10 @@ import com.ajou.hertz.common.auth.dto.request.KakaoLoginRequest; import com.ajou.hertz.common.auth.dto.request.LoginRequest; import com.ajou.hertz.common.auth.dto.request.SendUserAuthCodeRequest; +import com.ajou.hertz.common.auth.dto.request.VerifyUserAuthCodeRequest; import com.ajou.hertz.common.auth.dto.response.JwtTokenInfoResponse; import com.ajou.hertz.common.auth.dto.response.SendUserAuthCodeResponse; +import com.ajou.hertz.common.auth.dto.response.VerifyUserAUthCodeResponse; import com.ajou.hertz.common.auth.service.AuthService; import com.ajou.hertz.common.auth.service.UserAuthCodeService; import com.ajou.hertz.common.kakao.service.KakaoService; @@ -86,4 +87,24 @@ public SendUserAuthCodeResponse sendUserAuthCodeV1(@RequestBody @Valid SendUserA userAuthCodeService.sendUserAuthCodeViaSms(sendCodeRequest.getPhoneNumber()); return new SendUserAuthCodeResponse(true); } + + @Operation( + summary = "일회용 본인 인증 코드 검증", + description = """ +
전달된 코드가 '일회용 본인 인증 코드 발송 API'를 통해 사용자에게 발송된 본인 인증 코드인지 검증합니다. +
검증에 성공한 경우, 생성된 본인 인증코드는 더 이상 사용할 수 없습니다. (일회용)
+ """
+ )
+ @ApiResponses({
+ @ApiResponse(responseCode = "200"),
+ @ApiResponse(responseCode = "400", description = "[2005] 전달된 인증 코드 또는 전화번호가 유효하지 않은 경우. 인증 코드가 만료되었거나 잘못된 코드가 전송되었을 수 있습니다", content = @Content),
+ @ApiResponse(responseCode = "404", description = "[2004] 본인 인증 코드 발행 이력을 찾을 수 없는 경우. 인증 코드가 만료되었거나 잘못된 코드가 전송되었을 수 있습니다.", content = @Content)
+ })
+ @PostMapping(value = "/codes/verify", headers = API_VERSION_HEADER_NAME + "=" + 1)
+ public VerifyUserAUthCodeResponse verifyUserAUthCodeV1(
+ @RequestBody @Valid VerifyUserAuthCodeRequest verifyRequest
+ ) {
+ userAuthCodeService.verifyUserAuthCode(verifyRequest.getCode(), verifyRequest.getPhoneNumber());
+ return new VerifyUserAUthCodeResponse(true);
+ }
}
diff --git a/src/main/java/com/ajou/hertz/common/auth/dto/request/VerifyUserAuthCodeRequest.java b/src/main/java/com/ajou/hertz/common/auth/dto/request/VerifyUserAuthCodeRequest.java
new file mode 100644
index 0000000..ddaacf1
--- /dev/null
+++ b/src/main/java/com/ajou/hertz/common/auth/dto/request/VerifyUserAuthCodeRequest.java
@@ -0,0 +1,25 @@
+package com.ajou.hertz.common.auth.dto.request;
+
+import com.ajou.hertz.common.validator.PhoneNumber;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@AllArgsConstructor(access = AccessLevel.PRIVATE)
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+@Getter
+public class VerifyUserAuthCodeRequest {
+
+ @Schema(description = "본인 인증 코드를 전달받은 사용자의 전화번호", example = "01012345678")
+ @NotBlank
+ @PhoneNumber
+ private String phoneNumber;
+
+ @Schema(description = "사용자가 전달받은 인증 코드", example = "1a2b3c4d")
+ @NotBlank
+ private String code;
+}
diff --git a/src/main/java/com/ajou/hertz/common/auth/dto/response/VerifyUserAUthCodeResponse.java b/src/main/java/com/ajou/hertz/common/auth/dto/response/VerifyUserAUthCodeResponse.java
new file mode 100644
index 0000000..bdd3415
--- /dev/null
+++ b/src/main/java/com/ajou/hertz/common/auth/dto/response/VerifyUserAUthCodeResponse.java
@@ -0,0 +1,14 @@
+package com.ajou.hertz.common.auth.dto.response;
+
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@AllArgsConstructor
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+@Getter
+public class VerifyUserAUthCodeResponse {
+
+ private Boolean isSuccess;
+}
diff --git a/src/main/java/com/ajou/hertz/common/auth/entity/UserAuthCode.java b/src/main/java/com/ajou/hertz/common/auth/entity/UserAuthCode.java
index c95746c..68e4f97 100644
--- a/src/main/java/com/ajou/hertz/common/auth/entity/UserAuthCode.java
+++ b/src/main/java/com/ajou/hertz/common/auth/entity/UserAuthCode.java
@@ -3,6 +3,8 @@
import java.io.Serializable;
import java.time.LocalDateTime;
+import com.ajou.hertz.common.auth.exception.InvalidAuthCodeException;
+
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
@@ -19,4 +21,11 @@ public class UserAuthCode implements Serializable {
private String code;
private String phoneNumber;
private LocalDateTime createdAt;
+
+ public void verify(String code, String phoneNumber) {
+ if (!this.code.equals(code)
+ || !this.phoneNumber.equals(phoneNumber)) {
+ throw new InvalidAuthCodeException();
+ }
+ }
}
diff --git a/src/main/java/com/ajou/hertz/common/auth/exception/InvalidAuthCodeException.java b/src/main/java/com/ajou/hertz/common/auth/exception/InvalidAuthCodeException.java
new file mode 100644
index 0000000..790c54a
--- /dev/null
+++ b/src/main/java/com/ajou/hertz/common/auth/exception/InvalidAuthCodeException.java
@@ -0,0 +1,11 @@
+package com.ajou.hertz.common.auth.exception;
+
+import com.ajou.hertz.common.exception.BadRequestException;
+import com.ajou.hertz.common.exception.constant.CustomExceptionType;
+
+public class InvalidAuthCodeException extends BadRequestException {
+
+ public InvalidAuthCodeException() {
+ super(CustomExceptionType.USER_AUTH_CODE_NOT_FOUND);
+ }
+}
diff --git a/src/main/java/com/ajou/hertz/common/auth/exception/UserAuthCodeNotFoundException.java b/src/main/java/com/ajou/hertz/common/auth/exception/UserAuthCodeNotFoundException.java
new file mode 100644
index 0000000..d264529
--- /dev/null
+++ b/src/main/java/com/ajou/hertz/common/auth/exception/UserAuthCodeNotFoundException.java
@@ -0,0 +1,11 @@
+package com.ajou.hertz.common.auth.exception;
+
+import com.ajou.hertz.common.exception.NotFoundException;
+import com.ajou.hertz.common.exception.constant.CustomExceptionType;
+
+public class UserAuthCodeNotFoundException extends NotFoundException {
+
+ public UserAuthCodeNotFoundException() {
+ super(CustomExceptionType.USER_AUTH_CODE_NOT_FOUND);
+ }
+}
diff --git a/src/main/java/com/ajou/hertz/common/auth/repository/UserAuthCodeRedisRepository.java b/src/main/java/com/ajou/hertz/common/auth/repository/UserAuthCodeRedisRepository.java
index 4177e95..0e8d166 100644
--- a/src/main/java/com/ajou/hertz/common/auth/repository/UserAuthCodeRedisRepository.java
+++ b/src/main/java/com/ajou/hertz/common/auth/repository/UserAuthCodeRedisRepository.java
@@ -7,6 +7,7 @@
import org.springframework.stereotype.Repository;
import com.ajou.hertz.common.auth.entity.UserAuthCode;
+import com.ajou.hertz.common.auth.exception.UserAuthCodeNotFoundException;
import lombok.RequiredArgsConstructor;
@@ -28,8 +29,13 @@ public UserAuthCode save(UserAuthCode userAuthCode) {
}
@Override
- public OptionalUserAuthCode
)을 삭제한다.
+ *
+ * @param code 유효한 코드인지 검증할 인증 코드
+ * @param phoneNumber 인증 코드를 전달받은 사용자의 전화번호
+ */
+ @Transactional
+ public void verifyUserAuthCode(String code, String phoneNumber) {
+ UserAuthCode userAuthCode = userAuthCodeRepository.getByCode(code);
+ userAuthCode.verify(code, phoneNumber);
+ userAuthCodeRepository.delete(userAuthCode);
+ }
+
+ /**
+ * 영문과 숫자가 포함된 랜덤한 8자리 인증 코드를 생성한다.
+ *
+ * @return 생성된 랜덤 인증 코드
+ */
private String generateRandomAuthCode() {
return UUID.randomUUID().toString().substring(0, USER_AUTH_CODE_LEN);
}
diff --git a/src/main/java/com/ajou/hertz/common/config/SecurityConfig.java b/src/main/java/com/ajou/hertz/common/config/SecurityConfig.java
index 17aa806..3b50b6c 100644
--- a/src/main/java/com/ajou/hertz/common/config/SecurityConfig.java
+++ b/src/main/java/com/ajou/hertz/common/config/SecurityConfig.java
@@ -47,6 +47,7 @@ public class SecurityConfig {
AUTH_WHITE_LIST.put("/api/auth/kakao/login", POST);
AUTH_WHITE_LIST.put("/api/users", POST);
AUTH_WHITE_LIST.put("/api/auth/codes/send", POST);
+ AUTH_WHITE_LIST.put("/api/auth/codes/verify", POST);
AUTH_WHITE_LIST.put("/api/users/existence", GET);
AUTH_WHITE_LIST.put("/api/users/email", GET);
AUTH_WHITE_LIST.put("/api/administrative-areas/sido", GET);
diff --git a/src/main/java/com/ajou/hertz/common/exception/constant/CustomExceptionType.java b/src/main/java/com/ajou/hertz/common/exception/constant/CustomExceptionType.java
index 6193219..a86cfc2 100644
--- a/src/main/java/com/ajou/hertz/common/exception/constant/CustomExceptionType.java
+++ b/src/main/java/com/ajou/hertz/common/exception/constant/CustomExceptionType.java
@@ -30,6 +30,8 @@ public enum CustomExceptionType {
UNAUTHORIZED(2001, "유효하지 않은 인증 정보로 인해 인증 과정에서 문제가 발생하였습니다."),
TOKEN_VALIDATE(2002, "유효하지 않은 token입니다. Token 값이 잘못되었거나 만료되어 유효하지 않은 경우로 token 갱신이 필요합니다."),
PASSWORD_MISMATCH(2003, "비밀번호가 일치하지 않습니다."),
+ USER_AUTH_CODE_NOT_FOUND(2004, "본인 인증 코드 발행 이력을 찾을 수 없습니다. 인증 코드가 만료되었거나 잘못된 코드가 전송되었을 수 있습니다. 다시 시도해주세요."),
+ INVALID_AUTH_CODE(2005, "유효하지 않은 인증 코드 또는 전화번호입니다. 인증 코드가 만료되었거나 잘못된 정보가 전송되었을 수 있습니다. 다시 시도해주세요."),
/**
* 유저 관련 예외
diff --git a/src/main/java/com/ajou/hertz/common/message/service/MessageService.java b/src/main/java/com/ajou/hertz/common/message/service/MessageService.java
index 4365a0e..ee34de0 100644
--- a/src/main/java/com/ajou/hertz/common/message/service/MessageService.java
+++ b/src/main/java/com/ajou/hertz/common/message/service/MessageService.java
@@ -1,6 +1,15 @@
package com.ajou.hertz.common.message.service;
+import com.ajou.hertz.common.message.exception.SendMessageException;
+
public interface MessageService {
+ /**
+ * 전달된 메시지 내용으로 사용자에게 문자를 발송한다.
+ *
+ * @param message 발송할 문자 내용
+ * @param targetPhoneNumber 발송하고자 하는 사용자의 전화번호
+ * @throws SendMessageException 메시지 발송 중 오류가 발생한 경우
+ */
void sendShortMessage(String message, String targetPhoneNumber);
}
diff --git a/src/test/java/com/ajou/hertz/integration/common/auth/repository/UserAuthCodeRedisRepositoryTest.java b/src/test/java/com/ajou/hertz/integration/common/auth/repository/UserAuthCodeRedisRepositoryTest.java
index 7f98902..9c09ed1 100644
--- a/src/test/java/com/ajou/hertz/integration/common/auth/repository/UserAuthCodeRedisRepositoryTest.java
+++ b/src/test/java/com/ajou/hertz/integration/common/auth/repository/UserAuthCodeRedisRepositoryTest.java
@@ -3,7 +3,6 @@
import static org.assertj.core.api.Assertions.*;
import java.time.LocalDateTime;
-import java.util.Optional;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.DisplayName;
@@ -13,9 +12,10 @@
import org.springframework.test.context.ActiveProfiles;
import com.ajou.hertz.common.auth.entity.UserAuthCode;
+import com.ajou.hertz.common.auth.exception.UserAuthCodeNotFoundException;
import com.ajou.hertz.common.auth.repository.UserAuthCodeRedisRepository;
-@Disabled("TODO: 추후 testcontainers 또는 embedded redis 환경 구축 후 테스트 대상에 포함시킬 예정")
+// @Disabled("TODO: 추후 testcontainers 또는 embedded redis 환경 구축 후 테스트 대상에 포함시킬 예정")
@DisplayName("[Integration] Repository(Redis) - user auth code")
@ActiveProfiles("test")
@SpringBootTest
@@ -37,8 +37,8 @@ public UserAuthCodeRedisRepositoryTest(UserAuthCodeRedisRepository sut) {
sut.save(userAuthCode);
// then
- Optional