Skip to content

Commit

Permalink
feat: #37 본인인증코드 검증 API 구현
Browse files Browse the repository at this point in the history
  • Loading branch information
Wo-ogie committed Mar 31, 2024
1 parent 0107b32 commit 50f5e6e
Show file tree
Hide file tree
Showing 16 changed files with 272 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -86,4 +87,24 @@ public SendUserAuthCodeResponse sendUserAuthCodeV1(@RequestBody @Valid SendUserA
userAuthCodeService.sendUserAuthCodeViaSms(sendCodeRequest.getPhoneNumber());
return new SendUserAuthCodeResponse(true);
}

@Operation(
summary = "일회용 본인 인증 코드 검증",
description = """
<p>전달된 코드가 '일회용 본인 인증 코드 발송 API'를 통해 사용자에게 발송된 본인 인증 코드인지 검증합니다.
<p>검증에 성공한 경우, 생성된 본인 인증코드는 더 이상 사용할 수 없습니다. (일회용)
"""
)
@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);
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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();
}
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -28,8 +29,13 @@ public UserAuthCode save(UserAuthCode userAuthCode) {
}

@Override
public Optional<UserAuthCode> findByCode(String code) {
UserAuthCode userAuthCode = redisTemplate.opsForValue().get(code);
return Optional.ofNullable(userAuthCode);
public UserAuthCode getByCode(String code) {
return Optional.ofNullable(redisTemplate.opsForValue().get(code))
.orElseThrow(UserAuthCodeNotFoundException::new);
}

@Override
public void delete(UserAuthCode userAuthCode) {
redisTemplate.delete(userAuthCode.getCode());
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package com.ajou.hertz.common.auth.repository;

import java.util.Optional;

import com.ajou.hertz.common.auth.entity.UserAuthCode;

public interface UserAuthCodeRepository {

UserAuthCode save(UserAuthCode userAuthCode);

Optional<UserAuthCode> findByCode(String code);
UserAuthCode getByCode(String code);

void delete(UserAuthCode userAuthCode);
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ public class UserAuthCodeService {
private final MessageService messageService;
private final UserAuthCodeRepository userAuthCodeRepository;

/**
* 본인 인증 코드를 발송한다.
*
* @param targetPhoneNumber 인증 코드를 발송할 사용자의 전화번호
*/
@Transactional
public void sendUserAuthCodeViaSms(@NonNull String targetPhoneNumber) {
String authCode = generateRandomAuthCode();
Expand All @@ -33,6 +38,25 @@ public void sendUserAuthCodeViaSms(@NonNull String targetPhoneNumber) {
);
}

/**
* 전달된 코드가 유효한 코드인지 검증한다.
* 만약 성공적으로 유효성이 검증되었다면, 저장된 본인 인증 코드 발급 이력(<code>UserAuthCode</code>)을 삭제한다.
*
* @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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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, "유효하지 않은 인증 코드 또는 전화번호입니다. 인증 코드가 만료되었거나 잘못된 정보가 전송되었을 수 있습니다. 다시 시도해주세요."),

/**
* 유저 관련 예외
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand All @@ -37,8 +37,8 @@ public UserAuthCodeRedisRepositoryTest(UserAuthCodeRedisRepository sut) {
sut.save(userAuthCode);

// then
Optional<UserAuthCode> result = sut.findByCode(userAuthCode.getCode());
assertThat(result).isNotNull();
UserAuthCode result = sut.getByCode(userAuthCode.getCode());
assertThat(result.getCode()).isEqualTo(userAuthCode.getCode());
}

@Test
Expand All @@ -49,9 +49,38 @@ public UserAuthCodeRedisRepositoryTest(UserAuthCodeRedisRepository sut) {
sut.save(userAuthCode);

// when
Optional<UserAuthCode> result = sut.findByCode(code);
UserAuthCode result = sut.getByCode(code);

// then
assertThat(result).isNotNull();
assertThat(result.getCode()).isEqualTo(userAuthCode.getCode());
}
}

@Test
void 존재하지_않는_코드로_유저_인증_코드를_단건_조회하면_예외가_발생한다() {
// given
sut.save(
new UserAuthCode("code", "01012345678", LocalDateTime.now())
);

// when
Throwable ex = catchThrowable(() -> sut.getByCode("code-not-exists"));

// then
assertThat(ex).isInstanceOf(UserAuthCodeNotFoundException.class);
}

@Test
void 전달된_코드로_유저_인증_코드를_삭제한다() {
// given
String code = "code";
UserAuthCode userAuthCode = new UserAuthCode(code, "01012345678", LocalDateTime.now());
sut.save(userAuthCode);

// when
sut.delete(userAuthCode);

// then
assertThatThrownBy(() -> sut.getByCode(code))
.isInstanceOf(UserAuthCodeNotFoundException.class);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
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.service.AuthService;
import com.ajou.hertz.common.auth.service.UserAuthCodeService;
import com.ajou.hertz.common.kakao.service.KakaoService;
Expand Down Expand Up @@ -111,6 +112,26 @@ public AuthControllerTest(MockMvc mvc, ObjectMapper objectMapper) {
verifyEveryMocksShouldHaveNoMoreInteractions();
}

@Test
void 주어진_코드가_유효한_본인_인증_코드인지_검증한다() throws Exception {
// given
String code = "code";
String phoneNumber = "01012345678";
VerifyUserAuthCodeRequest verifyUserAuthCodeRequest = createVerifyUserAuthCodeRequest(phoneNumber, code);
willDoNothing().given(userAuthCodeService).verifyUserAuthCode(code, phoneNumber);

// when & then
mvc.perform(
post("/api/auth/codes/verify")
.header(API_VERSION_HEADER_NAME, 1)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(verifyUserAuthCodeRequest))
)
.andExpect(status().isOk());
then(userAuthCodeService).should().verifyUserAuthCode(code, phoneNumber);
verifyEveryMocksShouldHaveNoMoreInteractions();
}

private void verifyEveryMocksShouldHaveNoMoreInteractions() {
then(authService).shouldHaveNoMoreInteractions();
then(kakaoService).shouldHaveNoMoreInteractions();
Expand All @@ -129,6 +150,11 @@ private SendUserAuthCodeRequest createSendUserAuthCodeRequest(String targetPhone
return ReflectionUtils.createSendUserAuthCodeRequest(targetPhoneNumber);
}

private VerifyUserAuthCodeRequest createVerifyUserAuthCodeRequest(String phoneNumber, String code) throws
Exception {
return ReflectionUtils.createVerifyUserAuthCodeRequest(phoneNumber, code);
}

private JwtTokenInfoDto createJwtTokenInfoDto() {
return new JwtTokenInfoDto(
"access-token",
Expand Down
Loading

0 comments on commit 50f5e6e

Please sign in to comment.