스프링부트) 회원가입 번호인증(1)

2024. 5. 23. 18:14웹개발

728x90

회원가입 시 신원을 확인하기 위해 보편적으로 사용하는 전화번호 인증을 구현해 보았다.

 

참고: 

 

CoolSms, Springboot, Redis 문자로 인증하기

하 일단 CoolSms, Springboot, Redis 이 세가지를 사용해서 문자 인증 구현하는 방법을 찾아봤는데 내용이 너무 별로 없어서 슬펐다 .. coolsms의 경우 몇년 전 글들이랑 지금 사용하는 방법이랑 차이가 있

velog.io

 

 

 

[Spring] 회원가입시 필요한 인증번호 관리

프로젝트의 전체 소스 코드는 이곳에서 확인하실 수 있습니다. 이전에 진행했었던 프로젝트에서도 휴대폰 인증을 통한 회원가입 인증번호를 구현했었는데 당시에는 인증번호의 일치 불일치 여

1-7171771.tistory.com

 

 

 

[Spring boot] 문자 SMS 인증 구현하기(2)

2021.12.22 - [Back-end/Spring & Spring Boot] - [Spring boot] 문자 SMS 인증 구현하기(1) 이전 포스팅에 SMS 문자로 인증 번호를 발송하는 부분을 구현해보았는데 이번 포스팅엔 발송했던 인증 번호를 저장해 두

diddl.tistory.com

 

 

 

[Spring] 문자 인증 구현하기 - coolSMS

항해 99 최종 프로젝트를 진행 중 Nice API와 같은 인증 서비스를 도입하고 싶었으나, 사업자 등록이 필요하다는 답변을 받고 차선책으로 이메일 찾기 기능의 인증 수단으로서 사용하였다.Old-Version

velog.io

 

※ 위의 블로그들을 정독하고 오시면 많은 도움이 됩니다. 😄😄

 

큰 틀로는 html에서 내 휴대전화를 폼으로 넘기면 서버에서 인증번호를 생성하고, 인증번호를 'redis'에 저장하고 'nurigo'라는 api를 사용해 내  휴대전화에 인증번호를 포함한 문자가 날라오는 형식이다. 그래서 휴대전화에 날라온 인증번호와 redis에 저장된 인증번호가 일치할 때에만 인증이 되는 형식이다.

 

<gradle>

//전화번호 인증 coolsms
implementation 'net.nurigo:sdk:4.3.0'

//redis
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-data-redis', version: '3.2.2'

 

<application.properties>

#전화번호 인증 coolSMS
spring.coolsms.apikey = 
spring.coolsms.apisecret = 
spring.coolsms.fromnumber = 
spring.coolsms.provider = https://api.coolsms.co.kr

#redis
spring.redis.host=localhost
spring.redis.port=6379
spring.data.redis.repositories.enabled=false

 

<redis config>

import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
@EnableRedisRepositories
@RequiredArgsConstructor
public class RedisConfig {

    @Value("${spring.redis.host}")
    private String redisHost;

    @Value("${spring.redis.port}")
    private int redisPort;

    // lettuce
    @Bean
    public RedisConnectionFactory redisConnectionFactory() { //redis 호스트, 포트 설정
        return new LettuceConnectionFactory(redisHost, redisPort);
    }

    @Bean
    public StringRedisTemplate stringRedisTemplate() { //redis에 문자열 데이터를 저장
        StringRedisTemplate stringRedisTemplate = new StringRedisTemplate();
        stringRedisTemplate.setConnectionFactory(redisConnectionFactory());
        return stringRedisTemplate;
    }
}

 

<UserCheckDTO>

import lombok.Getter;
import lombok.Setter;

public class UserCheckDTO {

    @Getter
    public static class SmsCertificationRequest {
        private String phone;
        private String certificationNumber;
    }
}

 

<SmsCertification>

import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Repository;

import java.time.Duration;

@RequiredArgsConstructor
@Repository
public class SmsCertification {
    private final String PREFIX = "sms:"; // key값이 중복되지 않도록 상수 선언
    private final int LIMIT_TIME = 3*60;    //인증번호 유효 시간

    private final StringRedisTemplate stringRedisTemplate;

    //Redis에 저장
    public void createSmsCertification(String phone, String certificationNumber) {
        stringRedisTemplate.opsForValue()
                .set(PREFIX + phone, certificationNumber, Duration.ofSeconds(LIMIT_TIME));
    }

    //휴대전화 번호에 해당하는 인증번호 불러오기
    public String getSmsCertification(String phone) {
        return stringRedisTemplate.opsForValue().get(PREFIX + phone);
    }

    //인증 완료 시, 인증번호 Redis에서 삭제
    public void deleteSmsCertification(String phone) {
        stringRedisTemplate.delete(PREFIX + phone);
    }

    //Redis에 해당 휴대번호로 저장된 인증번호가 존재하는지 확인
    public boolean hasKey(String phone) {
        return stringRedisTemplate.hasKey(PREFIX + phone);
    }
    
}

 

<SmsUtil>

import net.nurigo.sdk.NurigoApp;
import net.nurigo.sdk.message.request.SingleMessageSendingRequest;
import net.nurigo.sdk.message.response.SingleMessageSentResponse;
import net.nurigo.sdk.message.service.DefaultMessageService;
import net.nurigo.sdk.message.model.Message;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;

@Component
public class SmsUtil {

    @Value("${spring.coolsms.fromnumber}")
    private String fromNumber;

    @Value("${spring.coolsms.apikey}")
    private String apiKey;

    @Value("${spring.coolsms.apisecret}")
    private String apiSecretKey;

    @Value("${spring.coolsms.provider}")
    private String provider;

    private DefaultMessageService messageService;


    @PostConstruct
    private void init() {
        this.messageService = NurigoApp.INSTANCE.initialize(apiKey, apiSecretKey, provider);
    }

    //단일 메시지 발송 예제
    public SingleMessageSentResponse sendSMS(String to, String verificationCode) {
        Message message = new Message();
        //발신번호 및 수신번호는 반드시 01012345678 형태로 입력되어야 함!!
        message.setFrom(fromNumber);
        message.setTo(to);
        message.setText("[모해먹] 인증번호를 입력해 주세요\n" + verificationCode);

        try {
            SingleMessageSentResponse response = this.messageService.sendOne(new SingleMessageSendingRequest(message));
            System.out.println(response);
            return response;
        } catch (Exception e) {
            System.out.println("Exception while sending SMS: " + e.getMessage());
            e.printStackTrace(); // 스택 트레이스 출력
            throw e; // 예외를 다시 던져서 상위 메서드로 전파
        }
    }
}

 

<UserCheck>

import com.example.enlaco.DTO.UserCheckDTO;

public interface UserCheck {
    void sendSMS(UserCheckDTO.SmsCertificationRequest requestDTO);
    void verifySMS(UserCheckDTO.SmsCertificationRequest requestDTO);
    boolean isVerify(UserCheckDTO.SmsCertificationRequest requestDTO);
}

 

<UserCheckService>

import com.example.enlaco.DTO.UserCheckDTO;
import com.example.enlaco.Repository.SmsCertification;
import com.example.enlaco.Util.SmsUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.example.enlaco.Exceptions.CustomExceptions;

@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class UserCheckService implements UserCheck{

    private final SmsUtil smsUtil;
    private final SmsCertification smsCertification;

    public void sendSMS(UserCheckDTO.SmsCertificationRequest requestDTO) {
        // 요청 DTO에서 전화번호를 가져옵니다.
        String to = requestDTO.getPhone();

        // 1000에서 9999 사이의 랜덤한 인증번호를 생성합니다.
        int randomNumber = (int) (Math.random() * 9000) + 1000;
        String certificationNumber = String.valueOf(randomNumber);

        System.out.println("Sending SMS to: " + to + " with code: " + certificationNumber);

        // SMS를 발송합니다.
        smsUtil.sendSMS(to, certificationNumber);

        // 생성한 인증번호를 저장합니다.
        smsCertification.createSmsCertification(to, certificationNumber);
    }
    
    public boolean isVerify(UserCheckDTO.SmsCertificationRequest requestDTO) {
        String formCertificationNumber = requestDTO.getCertificationNumber();

        //redis 인증번호와 폼에서 보낸 인증번호가 일치할 때 true, redis 데이터 삭제
        if (formCertificationNumber.equals(smsCertification.getSmsCertification(requestDTO.getPhone()))) {
            smsCertification.deleteSmsCertification(requestDTO.getPhone());
            return true;
        }
        
        return false;
    }
}

 

<SmsCertificationController>

import com.example.enlaco.DTO.UserCheckDTO;
import com.example.enlaco.Exceptions.ResponseMessage;
import com.example.enlaco.Exceptions.StatusCode;
import com.example.enlaco.Service.UserCheck;
import com.example.enlaco.Service.UserCheckService;
import com.example.enlaco.Util.DefaultRes;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
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;
import org.springframework.web.bind.annotation.RestController;
import com.example.enlaco.Exceptions.CustomExceptions;

@RestController
@RequiredArgsConstructor
@RequestMapping("/sms-certification")
public class SmsCertificationController extends BaseController{

    private final UserCheckService userCheckService;
    private final UserCheck userCheck;

    //전송
    @PostMapping("/send")
    public ResponseEntity<?> sendSms(@RequestBody UserCheckDTO.SmsCertificationRequest requestDTO) throws Exception {
        System.out.println("Received phone number: " + requestDTO.getPhone() + requestDTO.getCertificationNumber());
        try {
            userCheck.sendSMS(requestDTO);
            return new ResponseEntity(DefaultRes.res(StatusCode.OK, ResponseMessage.SMS_CERT_MESSAGE_SUCCESS), HttpStatus.OK);
        } catch (CustomExceptions.Exception e) {
            logger.error("Error occurred while sending SMS: ", e);
            return handleApiException(e, HttpStatus.BAD_REQUEST);
        }
    }

    //인증번호 확인
    @PostMapping("/confirm")
    public ResponseEntity<Void> SmsVerification(@RequestBody UserCheckDTO.SmsCertificationRequest requestDTO) throws Exception {
        try {
            if (userCheck.isVerify(requestDTO)){
                return new ResponseEntity(DefaultRes.res(StatusCode.OK, ResponseMessage.SMS_CERT_SUCCESS), HttpStatus.OK);
            }else {
                // 인증이 실패한 경우 실패 응답을 반환
                return new ResponseEntity(DefaultRes.res(StatusCode.BAD_REQUEST, ResponseMessage.SMS_CERT_FAILED), HttpStatus.BAD_REQUEST);
            }
        } catch (CustomExceptions.Exception e) {
            return handleApiException(e, HttpStatus.BAD_REQUEST);
        }
    }
}

 

이어서 인증을 해보겠습니다.

 

회원가입 번호인증(2)

직접 인증을 해봤습니다.  async function sendSMS() { //사용자로부터 번호를 입력 받는 요소 const phone = document.getElementById("mphone").value; //서버로 보낼 데이터 const data = { phone: phone }; //Fetch API를 사용해 a

studydogyu.tistory.com

 

 

※ 피드백 및 지적 감사합니다.