From 3d10880a36f9b31d9b870ecc6f59181668eb4e47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A7=80=EC=9D=80?= Date: Wed, 9 Oct 2024 13:26:56 +0900 Subject: [PATCH 1/4] =?UTF-8?q?=EB=AC=B8=EC=9E=90=EC=9D=B8=EC=A6=9D=20?= =?UTF-8?q?=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build.gradle b/build.gradle index 77e627e..67d9046 100644 --- a/build.gradle +++ b/build.gradle @@ -81,6 +81,9 @@ dependencies { implementation 'com.google.zxing:core:3.4.1' implementation 'com.google.zxing:javase:3.4.1' + + //nurigo 문자인증 + implementation 'net.nurigo:sdk:4.3.0' } From 0f13f2421ccf0875d5b6008fc9feb1f4286c39d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A7=80=EC=9D=80?= Date: Thu, 10 Oct 2024 08:06:32 +0900 Subject: [PATCH 2/4] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=EC=8B=9C=20=EB=AC=B8=EC=9E=90=EC=9D=B8=EC=A6=9D=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 회원가입시 사용자 번호로 문자 전송 기능 - 인증번호 입력시 검증 후 가입 - 전화번호 중복여부 검사 - 인증번호 만료 시간 설정 --- .../team/global/configure/SecurityConfig.java | 6 +-- .../team/global/exception/ExceptionCode.java | 3 +- .../team/user/SmsCertificationUtil.java | 38 +++++++++++++++ .../team/user/controller/SmsController.java | 41 ++++++++++++++++ .../team/user/repository/SmsRepository.java | 39 +++++++++++++++ .../team/user/repository/UserRepository.java | 1 + .../team/user/requestdto/SmsRequestDto.java | 10 ++++ .../user/requestdto/SmsVerifyRequestDto.java | 12 +++++ .../user/requestdto/UserJoinRequestDto.java | 6 ++- .../team/user/service/SmsService.java | 47 +++++++++++++++++++ .../team/user/service/UserService.java | 9 +++- src/main/resources/application.yml | 5 ++ 12 files changed, 209 insertions(+), 8 deletions(-) create mode 100644 src/main/java/com/sscanner/team/user/SmsCertificationUtil.java create mode 100644 src/main/java/com/sscanner/team/user/controller/SmsController.java create mode 100644 src/main/java/com/sscanner/team/user/repository/SmsRepository.java create mode 100644 src/main/java/com/sscanner/team/user/requestdto/SmsRequestDto.java create mode 100644 src/main/java/com/sscanner/team/user/requestdto/SmsVerifyRequestDto.java create mode 100644 src/main/java/com/sscanner/team/user/service/SmsService.java diff --git a/src/main/java/com/sscanner/team/global/configure/SecurityConfig.java b/src/main/java/com/sscanner/team/global/configure/SecurityConfig.java index 6dfbae9..f72f6f7 100644 --- a/src/main/java/com/sscanner/team/global/configure/SecurityConfig.java +++ b/src/main/java/com/sscanner/team/global/configure/SecurityConfig.java @@ -45,12 +45,8 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { // 경로별 인가 http.authorizeHttpRequests((authorize)-> -// authorize.requestMatchers("/**").permitAll() -// .requestMatchers("/reissue").permitAll() - - authorize.requestMatchers("/login","/", "health","api/users/join").permitAll() + authorize.requestMatchers("/login","/", "health","api/users/join","/sms/**").permitAll() .requestMatchers("/admin").hasRole("ADMIN") - .requestMatchers("/reissue").permitAll() .anyRequest().authenticated() ); diff --git a/src/main/java/com/sscanner/team/global/exception/ExceptionCode.java b/src/main/java/com/sscanner/team/global/exception/ExceptionCode.java index a761f47..cea54f8 100644 --- a/src/main/java/com/sscanner/team/global/exception/ExceptionCode.java +++ b/src/main/java/com/sscanner/team/global/exception/ExceptionCode.java @@ -58,7 +58,8 @@ public enum ExceptionCode { NOT_EXIST_REFRESH_TOKEN(400, "리프레시 토큰이 존재하지 않습니다."), EXPIRED_REFRESH_TOKEN(400, "리프레시 토큰이 만료되었습니다."), INVALID_REFRESH_TOKEN(400, "유효하지 않은 리프레시 토큰입니다."), - CURRENT_PASSWORD_NOT_MATCH(400, "현재 비밀번호와 일치하지 않습니다."); + CURRENT_PASSWORD_NOT_MATCH(400, "현재 비밀번호와 일치하지 않습니다."), + UNAUTHORIZED(401, "인증에 실패했습니다."); private final int code; private final String message; diff --git a/src/main/java/com/sscanner/team/user/SmsCertificationUtil.java b/src/main/java/com/sscanner/team/user/SmsCertificationUtil.java new file mode 100644 index 0000000..b108ade --- /dev/null +++ b/src/main/java/com/sscanner/team/user/SmsCertificationUtil.java @@ -0,0 +1,38 @@ +package com.sscanner.team.user; + +import jakarta.annotation.PostConstruct; +import net.nurigo.sdk.NurigoApp; +import net.nurigo.sdk.message.model.Message; +import net.nurigo.sdk.message.request.SingleMessageSendingRequest; +import net.nurigo.sdk.message.service.DefaultMessageService; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Component +public class SmsCertificationUtil { + @Value("${coolsms.api.key}") + private String apiKey; + + @Value("${coolsms.api.secret}") + private String apiSecret; + + @Value("${coolsms.api.number}") + private String fromNumber; + + DefaultMessageService messageService; + + @PostConstruct // 의존성 주입이 완료된 후 초기화 수행 + public void init(){ + this.messageService = NurigoApp.INSTANCE.initialize(apiKey, apiSecret, "https://api.coolsms.co.kr"); + } + + // 단일 메시지 발송 + public void sendSMS(String to, String certificationCode){ + Message message = new Message(); + message.setFrom(fromNumber); + message.setTo(to); + message.setText("본인확인 인증번호는 " + certificationCode + "입니다."); + + this.messageService.sendOne(new SingleMessageSendingRequest(message)); + } +} diff --git a/src/main/java/com/sscanner/team/user/controller/SmsController.java b/src/main/java/com/sscanner/team/user/controller/SmsController.java new file mode 100644 index 0000000..d26cba2 --- /dev/null +++ b/src/main/java/com/sscanner/team/user/controller/SmsController.java @@ -0,0 +1,41 @@ +package com.sscanner.team.user.controller; + +import com.sscanner.team.global.common.response.ApiResponse; +import com.sscanner.team.global.exception.BadRequestException; +import com.sscanner.team.global.exception.ExceptionCode; +import com.sscanner.team.user.requestDto.SmsRequestDto; +import com.sscanner.team.user.requestDto.SmsVerifyRequestDto; +import com.sscanner.team.user.service.SmsService; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/sms") +public class SmsController { + + private final SmsService smsService; + + @PostMapping("/send") + public ApiResponse SendSMS(@RequestBody @Valid SmsRequestDto smsRequestDto){ + smsService.SendSms(smsRequestDto); + return new ApiResponse<>(200,"문자를 전송했습니다",null); + } + + @PostMapping("/verify") + public ApiResponse verifyCode(@RequestBody @Valid SmsVerifyRequestDto req) { + boolean verify = smsService.verifyCode(req); + if (verify) { + return new ApiResponse<>(200,"인증이 완료되었습니다.",null); + } else { + throw new BadRequestException(ExceptionCode.UNAUTHORIZED); + } + } +} + + + diff --git a/src/main/java/com/sscanner/team/user/repository/SmsRepository.java b/src/main/java/com/sscanner/team/user/repository/SmsRepository.java new file mode 100644 index 0000000..7be884f --- /dev/null +++ b/src/main/java/com/sscanner/team/user/repository/SmsRepository.java @@ -0,0 +1,39 @@ +package com.sscanner.team.user.repository; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Repository; + +import java.time.Duration; + +@RequiredArgsConstructor +@Repository +public class SmsRepository { + + private final String PREFIX = "sms:"; // 키 + + private final StringRedisTemplate stringRedisTemplate; + + // 인증 정보 저장 + public void createSmsCertification(String phone, String code) { + int LIMIT_TIME = 60 * 120; // 유효시간 (2분) + stringRedisTemplate.opsForValue() + .set(PREFIX + phone, code, Duration.ofSeconds(LIMIT_TIME)); + } + + // 인증 정보 조회 + public String getSmsCertification(String phone) { + return stringRedisTemplate.opsForValue().get(PREFIX + phone); + } + + //인증 정보 삭제 + public void deleteSmsCertification(String phone) { + stringRedisTemplate.delete(PREFIX + phone); + } + + // 인증 정보 Redis에 존재 확인 + public boolean hasKey(String phone) { + return Boolean.TRUE.equals(stringRedisTemplate.hasKey(PREFIX + phone)); // Redis에서 해당 키의 존재 여부 확인 + } +} + diff --git a/src/main/java/com/sscanner/team/user/repository/UserRepository.java b/src/main/java/com/sscanner/team/user/repository/UserRepository.java index 633782f..5941013 100644 --- a/src/main/java/com/sscanner/team/user/repository/UserRepository.java +++ b/src/main/java/com/sscanner/team/user/repository/UserRepository.java @@ -14,6 +14,7 @@ public interface UserRepository extends JpaRepository { boolean existsByNickname(String nickname); boolean existsByPhone(String phone); Optional findByEmail(String email); + Optional findByPhone(String phone); @Modifying @Transactional diff --git a/src/main/java/com/sscanner/team/user/requestdto/SmsRequestDto.java b/src/main/java/com/sscanner/team/user/requestdto/SmsRequestDto.java new file mode 100644 index 0000000..aa2f4a3 --- /dev/null +++ b/src/main/java/com/sscanner/team/user/requestdto/SmsRequestDto.java @@ -0,0 +1,10 @@ +package com.sscanner.team.user.requestdto; + +import jakarta.validation.constraints.NotEmpty; + +public record SmsRequestDto( + @NotEmpty(message = "휴대폰 번호를 입력해주세요") + String phoneNum +) { + +} \ No newline at end of file diff --git a/src/main/java/com/sscanner/team/user/requestdto/SmsVerifyRequestDto.java b/src/main/java/com/sscanner/team/user/requestdto/SmsVerifyRequestDto.java new file mode 100644 index 0000000..b9827da --- /dev/null +++ b/src/main/java/com/sscanner/team/user/requestdto/SmsVerifyRequestDto.java @@ -0,0 +1,12 @@ +package com.sscanner.team.user.requestdto; + +import jakarta.validation.constraints.NotNull; + +public record SmsVerifyRequestDto ( + + @NotNull(message = "휴대폰 번호를 입력해주세요.") + String phoneNum, + @NotNull(message = "인증번호를 입력해주세요.") + String code +){ +} diff --git a/src/main/java/com/sscanner/team/user/requestdto/UserJoinRequestDto.java b/src/main/java/com/sscanner/team/user/requestdto/UserJoinRequestDto.java index 6959756..a25d4a6 100644 --- a/src/main/java/com/sscanner/team/user/requestdto/UserJoinRequestDto.java +++ b/src/main/java/com/sscanner/team/user/requestdto/UserJoinRequestDto.java @@ -18,7 +18,11 @@ public record UserJoinRequestDto( String nickname, @NotBlank(message = "전화번호가 비어있습니다.") - String phone + String phone, + + @NotBlank(message = "인증번호가 비어있습니다.") + String smsCode + ) { diff --git a/src/main/java/com/sscanner/team/user/service/SmsService.java b/src/main/java/com/sscanner/team/user/service/SmsService.java new file mode 100644 index 0000000..0686159 --- /dev/null +++ b/src/main/java/com/sscanner/team/user/service/SmsService.java @@ -0,0 +1,47 @@ +package com.sscanner.team.user.service; + +import com.sscanner.team.global.exception.BadRequestException; +import com.sscanner.team.global.exception.ExceptionCode; +import com.sscanner.team.user.SmsCertificationUtil; +import com.sscanner.team.user.repository.SmsRepository; +import com.sscanner.team.user.repository.UserRepository; +import com.sscanner.team.user.requestdto.SmsRequestDto; +import com.sscanner.team.user.requestdto.SmsVerifyRequestDto; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@RequiredArgsConstructor +@Service +public class SmsService { + + private final SmsCertificationUtil smsCertificationUtil; + private final SmsRepository smsRepository; + private final UserRepository userRepository; + + public void SendSms(SmsRequestDto smsRequestDto) { + String phoneNum = smsRequestDto.phoneNum(); + + if (userRepository.findByPhone(phoneNum).isPresent()) { + throw new BadRequestException(ExceptionCode.DUPLICATED_PHONE); + } + + String certificationCode = Integer.toString((int)(Math.random() * (999999 - 100000 + 1)) + 100000); // 인증 코드(6자리랜덤) + smsCertificationUtil.sendSMS(phoneNum, certificationCode); + smsRepository.createSmsCertification(phoneNum, certificationCode); + } + + public boolean verifyCode(SmsVerifyRequestDto smsVerifyDto) { + if (isVerify(smsVerifyDto.phoneNum(), smsVerifyDto.code())) { + smsRepository.deleteSmsCertification(smsVerifyDto.phoneNum()); + return true; + } else { + return false; + } + } + + public boolean isVerify(String phoneNum, String code) { + return smsRepository.hasKey(phoneNum) && // 전화번호에 대한 키가 존재하고 + smsRepository.getSmsCertification(phoneNum).equals(code); // 저장된 인증 코드와 입력된 인증 코드가 일치하는지 확인 + } + +} diff --git a/src/main/java/com/sscanner/team/user/service/UserService.java b/src/main/java/com/sscanner/team/user/service/UserService.java index 07b0445..666becf 100644 --- a/src/main/java/com/sscanner/team/user/service/UserService.java +++ b/src/main/java/com/sscanner/team/user/service/UserService.java @@ -7,10 +7,11 @@ import com.sscanner.team.global.exception.ExceptionCode; import com.sscanner.team.user.repository.UserRepository; import com.sscanner.team.user.requestdto.UserJoinRequestDto; -import com.sscanner.team.user.requestdto.UserNicknameUpdateRequestDto; import com.sscanner.team.user.requestdto.UserPasswordChangeRequestDto; import com.sscanner.team.user.responsedto.*; import jakarta.transaction.Transactional; +import com.sscanner.team.user.requestdto.SmsVerifyRequestDto; +import com.sscanner.team.user.responsedto.UserJoinResponseDto; import lombok.RequiredArgsConstructor; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; @@ -22,6 +23,7 @@ public class UserService { private final UserRepository userRepository; private final BCryptPasswordEncoder passwordEncoder; + private final SmsService smsService; private final UserUtils userUtils; // 이메일 중복 체크 @@ -68,6 +70,11 @@ public UserJoinResponseDto join(UserJoinRequestDto req){ checkDuplicatedNickname(req.nickname()); checkDuplicatedPhone(req.phone()); + if (!smsService.verifyCode(new SmsVerifyRequestDto(req.phone(), req.smsCode()))) { + throw new IllegalArgumentException("핸드폰 인증에 실패하였습니다."); // 인증 실패 시 예외 던짐 + } + + confirmPassword(req.password(), req.passwordCheck()); User userEntity = req.toEntity(passwordEncoder.encode(req.password())); diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 01f8b14..756e8dd 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -56,5 +56,10 @@ logging: jwt: secret: ${JWT_SECRET} +coolsms: + api: + key: "${COOLSMS_API_KEY}" + secret: "${COOLSMS_API_SECRET}" + number: "${COOLSMS_PHONE_NUMBER}" From d30c35798797f1bb73a9b2324e9040fcbfe136e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A7=80=EC=9D=80?= Date: Thu, 10 Oct 2024 10:02:23 +0900 Subject: [PATCH 3/4] =?UTF-8?q?-fix:=20SecureRandom=EC=9D=84=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=ED=95=9C=20=EB=82=9C=EC=88=98=EC=83=9D=EC=84=B1?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../team/user/service/SmsService.java | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/sscanner/team/user/service/SmsService.java b/src/main/java/com/sscanner/team/user/service/SmsService.java index 0686159..ebf0d28 100644 --- a/src/main/java/com/sscanner/team/user/service/SmsService.java +++ b/src/main/java/com/sscanner/team/user/service/SmsService.java @@ -9,6 +9,7 @@ import com.sscanner.team.user.requestdto.SmsVerifyRequestDto; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import java.security.SecureRandom; @RequiredArgsConstructor @Service @@ -18,6 +19,8 @@ public class SmsService { private final SmsRepository smsRepository; private final UserRepository userRepository; + private static final SecureRandom secureRandom = new SecureRandom(); + public void SendSms(SmsRequestDto smsRequestDto) { String phoneNum = smsRequestDto.phoneNum(); @@ -25,9 +28,15 @@ public void SendSms(SmsRequestDto smsRequestDto) { throw new BadRequestException(ExceptionCode.DUPLICATED_PHONE); } - String certificationCode = Integer.toString((int)(Math.random() * (999999 - 100000 + 1)) + 100000); // 인증 코드(6자리랜덤) - smsCertificationUtil.sendSMS(phoneNum, certificationCode); - smsRepository.createSmsCertification(phoneNum, certificationCode); + int certificationCode = secureRandom.nextInt(900000) + 100000; // 100000 ~ 999999 범위의 난수 + String codeAsString = Integer.toString(certificationCode); + + // SMS 전송 + smsCertificationUtil.sendSMS(phoneNum, codeAsString); + + // 인증 코드 저장 + smsRepository.createSmsCertification(phoneNum, codeAsString); + } public boolean verifyCode(SmsVerifyRequestDto smsVerifyDto) { @@ -39,9 +48,9 @@ public boolean verifyCode(SmsVerifyRequestDto smsVerifyDto) { } } - public boolean isVerify(String phoneNum, String code) { - return smsRepository.hasKey(phoneNum) && // 전화번호에 대한 키가 존재하고 - smsRepository.getSmsCertification(phoneNum).equals(code); // 저장된 인증 코드와 입력된 인증 코드가 일치하는지 확인 + public boolean isVerify(String phoneNum, String code) { // 전화번호에 대한 키 존재 + 인증코드 일치 검증 + return smsRepository.hasKey(phoneNum) && + smsRepository.getSmsCertification(phoneNum).equals(code); } } From 0a5c5a8eebd8aab3f34c9f92f860f87e27bbac8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A7=80=EC=9D=80?= Date: Fri, 18 Oct 2024 17:34:46 +0900 Subject: [PATCH 4/4] =?UTF-8?q?-fix:=20=EC=9E=98=EB=AA=BB=EB=90=9C=20impor?= =?UTF-8?q?t=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/sscanner/team/user/controller/SmsController.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/sscanner/team/user/controller/SmsController.java b/src/main/java/com/sscanner/team/user/controller/SmsController.java index d26cba2..5c246e3 100644 --- a/src/main/java/com/sscanner/team/user/controller/SmsController.java +++ b/src/main/java/com/sscanner/team/user/controller/SmsController.java @@ -3,8 +3,8 @@ import com.sscanner.team.global.common.response.ApiResponse; import com.sscanner.team.global.exception.BadRequestException; import com.sscanner.team.global.exception.ExceptionCode; -import com.sscanner.team.user.requestDto.SmsRequestDto; -import com.sscanner.team.user.requestDto.SmsVerifyRequestDto; +import com.sscanner.team.user.requestdto.SmsRequestDto; +import com.sscanner.team.user.requestdto.SmsVerifyRequestDto; import com.sscanner.team.user.service.SmsService; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor;