Skip to content

Commit

Permalink
Merge pull request #98 from Ajou-Hertz/feature/#34-sms-user-authentic…
Browse files Browse the repository at this point in the history
…ation

SMS 본인인증 기능 구현 - 본인 인증 코드 문자 발송 API 구현
  • Loading branch information
Wo-ogie authored Mar 30, 2024
2 parents 66fb324 + 6649cd4 commit cd61ea6
Show file tree
Hide file tree
Showing 27 changed files with 709 additions and 4 deletions.
8 changes: 8 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ dependencies {
runtimeOnly 'com.h2database:h2'
runtimeOnly 'com.mysql:mysql-connector-j'

// Redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'

// Lombok
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
Expand Down Expand Up @@ -151,6 +154,11 @@ jacocoTestCoverageVerification {
'*common.entity.FullAddress*',
'*repository.*Repository*'
]

excludes = [
'*service.NcpMessageService',
'*repository.UserAuthCodeRedisRepository'
]
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

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 @@ -10,8 +11,10 @@
import com.ajou.hertz.common.auth.dto.JwtTokenInfoDto;
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.response.JwtTokenInfoResponse;
import com.ajou.hertz.common.auth.service.AuthService;
import com.ajou.hertz.common.auth.service.UserAuthCodeService;
import com.ajou.hertz.common.kakao.service.KakaoService;

import io.swagger.v3.oas.annotations.Operation;
Expand All @@ -30,6 +33,7 @@ public class AuthController {

private final AuthService authService;
private final KakaoService kakaoService;
private final UserAuthCodeService userAuthCodeService;

@Operation(
summary = "로그인",
Expand All @@ -52,18 +56,33 @@ public JwtTokenInfoResponse loginV1(@RequestBody @Valid LoginRequest loginReques
)
@ApiResponses({
@ApiResponse(responseCode = "200"),
@ApiResponse(
responseCode = "409", content = @Content,
@ApiResponse(responseCode = "409", content = @Content,
description = """
<p>[2200] 이미 다른 사용자가 사용 중인 이메일로 신규 회원을 등록하려고 하는 경우
<p>[2203] 이미 다른 사용자가 사용 중인 전화번호로 신규 회원을 등록하려고 하는 경우
"""
),
"""),
@ApiResponse(responseCode = "Any", description = "[10000] 카카오 서버와의 통신 중 오류가 발생한 경우. Http status code는 kakao에서 응답받은 것과 동일하게 설정하여 응답한다.", content = @Content)
})
@PostMapping(value = "/kakao/login", headers = API_VERSION_HEADER_NAME + "=" + 1)
public JwtTokenInfoResponse kakaoLoginV1(@RequestBody @Valid KakaoLoginRequest kakaoLoginRequest) {
JwtTokenInfoDto jwtTokenInfoDto = kakaoService.login(kakaoLoginRequest);
return JwtTokenInfoResponse.from(jwtTokenInfoDto);
}

@Operation(
summary = "일회용 본인 인증 코드 발송 (SMS)",
description = """
<p>전달받은 전화번호로 일회용 본인 인증 코드를 발송합니다.
<p>인증 코드의 유효 기간은 5분입니다. 인증 코드가 만료된 경우, 다시 인증 코드를 발급받아야 합니다.
"""
)
@ApiResponses({
@ApiResponse(responseCode = "204"),
@ApiResponse(responseCode = "Any", description = "[1002] 인증 코드 문자 발송 과정 중 오류가 발생한 경우", content = @Content)
})
@PostMapping(value = "/codes/send", headers = API_VERSION_HEADER_NAME + "=" + 1)
public ResponseEntity<Void> sendUserAuthCodeV1(@RequestBody @Valid SendUserAuthCodeRequest sendCodeRequest) {
userAuthCodeService.sendUserAuthCodeViaSms(sendCodeRequest.getPhoneNumber());
return ResponseEntity.noContent().build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
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 SendUserAuthCodeRequest {

@Schema(description = "본인 인증 코드를 발송할 전화번호", example = "01012345678")
@NotBlank
@PhoneNumber
private String phoneNumber;
}
22 changes: 22 additions & 0 deletions src/main/java/com/ajou/hertz/common/auth/entity/UserAuthCode.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.ajou.hertz.common.auth.entity;

import java.io.Serializable;
import java.time.LocalDateTime;

import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@Getter
public class UserAuthCode implements Serializable {

// Redis 뿐만 아니라, user auth code 자체의 유효 기간을 의미.
public static final int TTL_SECONDS = 300;

private String code;
private String phoneNumber;
private LocalDateTime createdAt;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.ajou.hertz.common.auth.repository;

import java.util.Optional;
import java.util.concurrent.TimeUnit;

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Repository;

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

import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Repository
public class UserAuthCodeRedisRepository implements UserAuthCodeRepository {

private final RedisTemplate<String, UserAuthCode> redisTemplate;

@Override
public UserAuthCode save(UserAuthCode userAuthCode) {
redisTemplate.opsForValue().set(
userAuthCode.getCode(),
userAuthCode,
UserAuthCode.TTL_SECONDS,
TimeUnit.SECONDS
);
return userAuthCode;
}

@Override
public Optional<UserAuthCode> findByCode(String code) {
UserAuthCode userAuthCode = redisTemplate.opsForValue().get(code);
return Optional.ofNullable(userAuthCode);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +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);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.ajou.hertz.common.auth.service;

import java.time.LocalDateTime;
import java.util.UUID;

import org.springframework.lang.NonNull;
import org.springframework.stereotype.Service;

import com.ajou.hertz.common.auth.entity.UserAuthCode;
import com.ajou.hertz.common.auth.repository.UserAuthCodeRepository;
import com.ajou.hertz.common.message.service.MessageService;

import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Service
public class UserAuthCodeService {

private static final int USER_AUTH_CODE_LEN = 8;

private final MessageService messageService;
private final UserAuthCodeRepository userAuthCodeRepository;

public void sendUserAuthCodeViaSms(@NonNull String targetPhoneNumber) {
String authCode = generateRandomAuthCode();
UserAuthCode userAuthCode = new UserAuthCode(authCode, targetPhoneNumber, LocalDateTime.now());
userAuthCodeRepository.save(userAuthCode);
messageService.sendShortMessage(
String.format("[헤르츠] 본인 확인을 위해 인증코드 %s 를 입력해주세요.", authCode),
targetPhoneNumber
);
}

private String generateRandomAuthCode() {
return UUID.randomUUID().toString().substring(0, USER_AUTH_CODE_LEN);
}
}
20 changes: 20 additions & 0 deletions src/main/java/com/ajou/hertz/common/config/ObjectMapperConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.ajou.hertz.common.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;

@Configuration
public class ObjectMapperConfig {

@Bean
public ObjectMapper objectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
JavaTimeModule javaTimeModule = new JavaTimeModule();
objectMapper.registerModules(javaTimeModule, new Jdk8Module());
return objectMapper;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.ajou.hertz.common.config;

import org.springframework.stereotype.Component;

import feign.RequestInterceptor;
import feign.RequestTemplate;

@Component
public class PathVariableEncodingExclusionInterceptor implements RequestInterceptor {
private static final String COLON = "%3A";

@Override
public void apply(final RequestTemplate template) {
final String path = template.path();
if (path.contains(COLON)) {
template.uri(path.replaceAll(COLON, ":"));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.ajou.hertz.common.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import com.ajou.hertz.common.auth.entity.UserAuthCode;
import com.fasterxml.jackson.databind.ObjectMapper;

@Configuration
public class RedisTemplateConfig {

@Bean
public RedisTemplate<String, UserAuthCode> redisTemplate(
RedisConnectionFactory connectionFactory,
ObjectMapper objectMapper
) {
RedisTemplate<String, UserAuthCode> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(connectionFactory);
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(objectMapper, UserAuthCode.class));
return redisTemplate;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ public class SecurityConfig {
AUTH_WHITE_LIST.put("/api/auth/login", POST);
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/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 @@ -21,6 +21,7 @@ public enum CustomExceptionType {

MULTIPART_FILE_NOT_READABLE(1000, "파일을 읽을 수 없습니다. 올바른 파일인지 다시 확인 후 요청해주세요."),
IO_PROCESSING(1001, "입출력 처리 중 오류가 발생했습니다."),
SEND_MESSAGE(1002, "문자 발송 과정에서 알 수 없는 에러가 발생했습니다."),

/**
* 로그인, 인증 관련 예외
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.ajou.hertz.common.message.constant;

public enum MessageType {
SMS,
LMS,
MMS
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.ajou.hertz.common.message.exception;

import org.springframework.http.HttpStatus;
import org.springframework.lang.NonNull;

import com.ajou.hertz.common.exception.CustomException;
import com.ajou.hertz.common.exception.constant.CustomExceptionType;

public class SendMessageException extends CustomException {

public SendMessageException(@NonNull HttpStatus httpStatus, String optionalMessage) {
super(httpStatus, CustomExceptionType.SEND_MESSAGE, optionalMessage);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.ajou.hertz.common.message.service;

public interface MessageService {

void sendShortMessage(String message, String targetPhoneNumber);
}
Loading

0 comments on commit cd61ea6

Please sign in to comment.