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

Yoojung Choi·2023년 11월 19일

하 일단 CoolSms, Springboot, Redis 이 세가지를 사용해서 문자 인증 구현하는 방법을 찾아봤는데 내용이 너무 별로 없어서 슬펐다 ..
coolsms의 경우 몇년 전 글들이랑 지금 사용하는 방법이랑 차이가 있었다!! DefaultMessageService의 sendOne을 이용해 문자를 한개 보낸다는 것이 가장 큰 차이인 듯!! 그래서 다른 플랫폼을 알아봤지만 내가 개발을 시작한 23년 11월에는 네이버 클라우드 플랫폼에서 Simple & Easy Notification Service 를 사용할 수 없었다.

그래서 nhn cloud의 sms 서비스를 이용하려고 했으나 신용카드를 등록해야했고, 나는 체크 카드 밖에 없어서 사용할 수가 없었다 ㅜㅜ 그래서 그냥 다시 coolsms를 이용하기로 결정했다.

코드의 큰 틀은 [Spring] 회원가입시 필요한 인증번호 관리 이 블로그를 보고 많이 참고했당 ㅋ [Spring] 문자 인증 구현하기 - coolSMS 이 블로그도 많이 참고함 ㅋ

이 두분의 블로그를 적절히 섞은 코드라고 해도 됨..ㅎㅎ
두 깃헙을 뒤져서 .. 한땀한땀 만든 코드 .. 아직 예외처리가 완벽하지는 않지만 추후 올릴예정입니당 !!!

<application.yml>

  redis:
    host: localhost
    port: 6379
    repositories:
      enabled: false
  coolsms:
    apiKey: ${내가 발급받은 키}
    apiSecret: ${내가 발급받은 secret키}
    senderNumber: 보내는 폰 번호

yml에서 senderNumber 진짜 중요함!!
없는 번호에서 발신은 안되기 때문에 꼭 있는 번호로 하기!! (이걸로 시간 엄청 잡아먹음 ㅜㅜ)

<RedisConfig.java>


@EnableRedisHttpSession
@Configuration
public class RedisConfig {

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

    @Bean
    public RedisConnectionFactory redisConnectionFactory(){
        return new LettuceConnectionFactory(redisHost, redisPort);
    }

    @Bean
    public StringRedisTemplate stringRedisTemplate() {
        StringRedisTemplate stringRedisTemplate = new StringRedisTemplate();
        stringRedisTemplate.setConnectionFactory(redisConnectionFactory());
        return stringRedisTemplate;
    }

}

<SmsCertificationDao.java>

@RequiredArgsConstructor
@Repository
public class SmsCertificationDao {

    private final String PREFIX = "sms:";
    private final int LIMIT_TIME = 3 * 60;

    private final StringRedisTemplate redisTemplate;

    public void createSmsCertification(String phone, String certificationNumber) {
        redisTemplate.opsForValue()
                .set(PREFIX + phone, certificationNumber, Duration.ofSeconds(LIMIT_TIME));
    }

    public String getSmsCertification(String phone) {
        return redisTemplate.opsForValue().get(PREFIX + phone);
    }

    public void removeSmsCertification(String phone) {
        redisTemplate.delete(PREFIX + phone);
    }

    public boolean hasKey(String phone) {
        return redisTemplate.hasKey(PREFIX + phone);
    }
}

<UserDto.java>

public class UserDto {

    @Getter
    public static class SmsCertificationRequest {

        private String phone;
        private String certificationNumber;

    }
}

<SmsCertificationUtil.java>

@Component
public class SmsCertificationUtil {

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

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

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

        DefaultMessageService messageService;


    @PostConstruct
    public void init() {
        this.messageService = NurigoApp.INSTANCE.initialize(apiKey, apiSecret, "https://api.coolsms.co.kr");
    }

    public SingleMessageSentResponse sendSms(String to, String verificationCode){
        Message message = new Message();
        message.setFrom(senderNumber);
        message.setTo(to);
        message.setText("[물막이] 본인 확인 인증번호는 "+verificationCode+"입니다.");

        SingleMessageSentResponse response = this.messageService.sendOne(new SingleMessageSendingRequest(message));
        System.out.println(response);
        return response;
    }

}

<UserService.java>

public interface UserService {
    void sendSms(UserDto.SmsCertificationRequest requestDto);
    void verifySms(UserDto.SmsCertificationRequest requestDto);
    boolean isVerify(UserDto.SmsCertificationRequest requestDto);

}

<UserServiceImpl.java>

@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {

    private final SmsCertificationUtil smsUtil;
    private final SmsCertificationDao smsCertificationDao;

    public void sendSms(UserDto.SmsCertificationRequest requestDto){
        String to = requestDto.getPhone();
        int randomNumber = (int) (Math.random() * 9000) + 1000;
        String certificationNumber = String.valueOf(randomNumber);
        smsUtil.sendSms(to, certificationNumber);
        smsCertificationDao.createSmsCertification(to,certificationNumber);
    }

    public void verifySms(UserDto.SmsCertificationRequest requestDto) {
        if (isVerify(requestDto)) {
            throw new CustomExceptions.SmsCertificationNumberMismatchException("인증번호가 일치하지 않습니다.");
        }
        smsCertificationDao.removeSmsCertification(requestDto.getPhone());
    }

    public boolean isVerify(UserDto.SmsCertificationRequest requestDto) {
        return !(smsCertificationDao.hasKey(requestDto.getPhone()) &&
                smsCertificationDao.getSmsCertification(requestDto.getPhone())
                        .equals(requestDto.getCertificationNumber()));
    }

}

<SmsCertificationController.java>

@RestController
@RequiredArgsConstructor
@RequestMapping("/sms-certification")
public class SmsCertificationController extends BaseController {
   private final UserService userService;

  @PostMapping("/send")
  public ResponseEntity<?> sendSms(@RequestBody UserDto.SmsCertificationRequest requestDto) throws Exception {
      try {
          userService.sendSms(requestDto);
          return new ResponseEntity(DefaultRes.res(StatusCode.OK, ResponseMessage.SMS_CERT_MESSAGE_SUCCESS), HttpStatus.OK);
      } catch (CustomExceptions.Exception e) {
          return handleApiException(e, HttpStatus.BAD_REQUEST);
      }
  }

  //인증번호 확인
  @PostMapping("/confirm")
  public ResponseEntity<Void> SmsVerification(@RequestBody UserDto.SmsCertificationRequest requestDto) throws Exception{
      try {
          userService.verifySms(requestDto);
          return new ResponseEntity(DefaultRes.res(StatusCode.OK, ResponseMessage.SMS_CERT_SUCCESS), HttpStatus.OK);
      } catch (CustomExceptions.Exception e) {
          return handleApiException(e, HttpStatus.BAD_REQUEST);
      }
  }


}

코드에 대한 설명은 나중에 붙여둠!!

인증까지 잘 되는 모습 !!

2개의 댓글

comment-user-thumbnail
2023년 12월 4일

CustomExceptions 은 별도로 생성하신 예외처리 함수인가요?

1개의 답글