이번 포스팅에서는 본인인증에 대해 작성해보려고 한다. Coolsms + Redis 조합으로 작업했으며 인증문자 발송부터 인증까지 상세하게 작성해 보도록 하겠다.
본인인증
특정한 방법을 통하여 특정인이 본임임을 증명하는 방법
재능 공유, 경험 공유, 중고거래 장터 이용 등 팀 프로젝트 컨셉에 따라 회원가입을 할 때 본인에 대한 인증이 필수다. 개인 연락처를 입력하게 하고 인증번호를 전송하여 확인을 통해 본인 인증을 하는 방식을 선택했다. 문자 인증 API를 찾아보고 제일 저렴하고 레퍼런스(기술 블로그)가 많은 coolSMS를 선택하였다. 사이트에 접속하여 회원가입을 하고 개발/연동 배너에서 API key를 생성한다. 시뻘건 글씨로 안내되고 있듯 key는 절대로 외부에 공개되면 안 된다.
CoolSMS
1. 기본 설정
API key를 생성하고 IP 주소를 관리해주어야 한다. API key와 마찬가지로 아무나, 누구나 사용할 수 없게 설정하는 것 같다. 생성된 API key의 우측 상단에 점 3개를 누르면 허용 IP 관리 배너가 보인다. 클릭해 주고...
본인 IP를 확인하여 허용 IP로 설정해 준다. 필자는 이 사이트를 이용했다.
이제 스프링부트에 의존성을 주입해 주고...(build.gradle.kotlin 파일)
implementation("net.nurigo:sdk:4.3.0") // coolsms 의존성 주입
application.properties 파일에 API 정보를 등록해 준다. 필자는 외부에 노출되면 안 되는 값들을. env 파일을 활용하여처리하고 있다. (이 포스팅을 참고하길 바란다)
2. 코드 작성
2-1. util이라는 패키지를 따로 생성하여 CertificationUtil클래스를 만들어주었다. (주석을 상세하게 적어놓았으니 참고하길 바란다)
package kr.co.vibevillage.user.model.util;
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 CertificationUtil {
//application.properties에 선언해둔 변수들
// coolsms apiKey 주입
@Value("${coolsms.apiKey}")
private String apiKey;
// coolsms secretKey 주입
@Value("${coolsms.secretKey}")
private String secretKey;
// 발신자 번호 주입
@Value("${coolsms.fromNumber}")
private String fromNumber;
// 메시지 서비스를 위한 객체
DefaultMessageService messageService;
// 의존성 주입이 완료된 후 초기화를 수행하는 메서드
@PostConstruct
public void init(){
// 메시지 서비스 초기화
this.messageService = NurigoApp.INSTANCE.initialize(apiKey, secretKey, "https://api.coolsms.co.kr");
}
// 단일 메시지 발송
public void sendSMS(String userPhone, String certificationCode){
// 새 메시지 객체 생성
Message message = new Message();
// 발신자 번호 설정
message.setFrom(fromNumber);
// 수신자 번호 설정
message.setTo(userPhone);
// 메시지 내용 설정
message.setText("[VibeVillage]본인확인 인증번호는 " + certificationCode + "입니다.");
// 메시지 발송 요청
this.messageService.sendOne(new SingleMessageSendingRequest(message));
}
}
2-2. HTML 수정, JavaScript 작성 - 본인인증 버튼을 누르면 인증 영역이 보이고, 인증하기 버튼을 누르면 함수가 실행된다.
-> 비동기 통신을 이용해 유저가 입력한 연락처를 전송한다.
<input id="phone" type="text" placeholder="연락처" onkeyup="phoneValidation()"/>
<button type="button" th:onclick="certification()">본인인증</button>
<div id="showPhone"></div>
<div style="display: none" id="certificationArea">
<input id="certificationInput" type="text" placeholder="인증번호"/>
<button type="button" th:onclick="startCertification()">인증하기</button>
</div>
function certification() {
const phone = document.getElementById("phone").value; // 유저가 입력한 연락처
const certificationArea = document.getElementById("certificationArea"); // 인증 영역
if(phone === "") {
swal("연락처를 입력해주세요.", "", "error");
} else {
$.ajax({
type: "POST",
url: "/sendCertificationNumber",
data: {userPhone : phone},
success: function success(res) {
swal("전송완료", "3분 이내로 인증해주세요.", "success");
certificationArea.style.display = "block"; // 숨겨진 인증 영역 보이게 설정
},
error: function error(err) {
},
});
}
}
2-3. Controller 작성 (매핑시켜주기) - 컨트롤러에서는 서비스 객체를 이용해 sendSms라는 메소드를 호출한다.
-> 비동기 통신으로부터 연락처를 전송받아 userPhone이라는 매개변수에 저장한다.
package kr.co.vibevillage.user.controller;
import kr.co.vibevillage.user.model.dto.UserDTO;
import kr.co.vibevillage.user.model.service.RegisterServiceImpl;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
@RequiredArgsConstructor // 초기화 되지 않은 final 필드나 @NonNull이 붙은 필드에 대한 생성자를 만들어줌
public class RegisterController {
... (생략)
// 문자 인증
@PostMapping("/sendCertificationNumber")
@ResponseBody
public void certification(String userPhone) {
registerService.sendSms(userPhone);
}
}
2-4. Service 작성
-> Math.random()을 활용하여 6자리의 난수를 생성하고, certificationCode라는 변수에 초기화시킨다.
-> 2-1에서 작성한 클래스를 자료형으로 하는 certificationUtil 객체를 생성하고, 그 객체로 sendSms 메서드를 호출한다.
package kr.co.vibevillage.user.model.service;
import kr.co.vibevillage.user.model.dto.UserDTO;
public interface RegisterService {
... (생략)
// 문자인증
public void sendSms(String userPhone);// 문자인증
}
package kr.co.vibevillage.user.model.service;
import kr.co.vibevillage.user.model.dto.UserDTO;
import kr.co.vibevillage.user.model.mapper.RegisterMapper;
import kr.co.vibevillage.user.model.util.CertificationUtil;
import kr.co.vibevillage.user.redis.dao.RedisRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor // 초기화 되지 않은 final 필드나 @NonNull이 붙은 필드에 대한 생성자를 만들어줌
public class RegisterServiceImpl implements RegisterService{
// Mapper 객체 생성
private final RegisterMapper registerMapper;
// 문자인증을 위한 객체 생성
private final CertificationUtil certificationUtil;
... (생략)
// 문자인증
@Override
public void sendSms(String userPhone) {
// 6자리 인증 코드를 랜덤으로 생성
String certificationCode = Integer.toString((int)(Math.random() * (999999 - 100000 + 1)) + 100000);
certificationUtil.sendSMS(userPhone, certificationCode);
}
}
3. 문자 발송 테스트
연락처를 입력하고 본인인증 버튼을 클릭하면 alert 창이 뜨고 인증할 수 있는 영역이 보인다. 그리고 인증번호 문자까지 발송이 완료된다.
인증번호를 받았으니 인증을 할 차례이다. 인증하는 방법은 여러 가지 생각해 보았는데..... 세 가지 정도가 떠올랐다.
1. 유저가 입력한 인증코드와 생성된 인증코드가 일치하는지 자바스크립트 코드를 작성하는 방법
2. 인증코드를 데이터베이스에 저장하여 유저가 입력한 코드를 받아 조회하여 인증하는 방법
3. Redis에 인증코드를 유저가 입력한 코드를 받아 인증하는 방법
1번의 경우 유저가 코딩에 관심 있는 사람이라면.. 또 악의적인 목적으로 접근하는 유저라면 인증코드를 브라우저 내에서 쉽게 찾아내어 이용할 수 있기 때문에 탈락.... 2번의 경우 인증코드는 회원가입할 때 한 번만 사용하는 데이터인데 DB에 저장할 필요가 없기에 탈락. (삭제 없이 계속 저장만 한다면 성능이 굉장히 낮아질 것이다..) 이와 같은 이유로 3번 Redis에 인증코드를 저장하여 인증하는 방법을 선택하였다.
Redis
Redis - The Real-time Data Platform
Developers love Redis. Unlock the full potential of the Redis database with Redis Enterprise and start building blazing fast apps.
redis.io
고성능의 키-값 저장소로 사용되는 오픈소스 인메모리 데이터 구조 서버이다.
Remot Dictionary Server의 약자로 빠른 데이터 접근 속도와 높은 처리량이 필요한 응용 프로그램에서 사용된다.
1. Redis 특징
1. 성능
모든 Redis 데이터는 메모리에 저장되어 대기 시간을 낮추고 처리량을 높인다.
평균적으로 읽기 및 쓰기의 작업 속도가 1ms로 디스크 기반 데이터베이스보다 빠르다.
2. 유연한 데이터 구조
Redis의 데이터는 String, List, Set, Hash, Sorted Set, Bitmap, JSON 등 다양한 데이터 타입을 지원한다.
따라서, 애플리케이션의 요구 사항에 알맞은 다양한 데이터 타입을 활용할 수 있다.
3. 개발 용이성
Redis는 쿼리문이 필요로 하지 않으며, 단순한 명령 구조로 데이터의 저장, 조회 등이 가능하다.
또한, Java, Python, C, C++, C#, JavaScript, PHP, Node.js, Ruby 등을 비롯한 다수의 언어를 지원한다.
4. 영속성
Redis는 영속성을 보장하기 위해 데이터를 디스크에 저장할 수 있다. 서버에 치명적인 문제가 발생하더라도 디스크에 저장된 데이터를 통해 복구가 가능하다.
5. TTL(Time to Live)
Redis는 Key에 대해 TTL을 설정할 수 있다. TTL이 만료되면 Redis는 자동으로 데이터를 삭제한다.
6. 싱글 스레드 방식
Redis는 싱글 스레드 방식을 사용하여 한 번에 하나의 명령어만을 처리한다. 따라서 연산을 원자적으로 처리하여 Race Condition(경쟁 상태)가 거의 발생하지 않는다.
하지만, 멀티 스레드를 지원하지 않기 때문에 시간 복잡도가 O(n)인 명령어의 사용은 주의해서 사용해야 한다.
출처 : It is True 블로그
성능(1번)도 뛰어나고 데이터를 자동으로 삭제(5번)해주기까지 한다니... 이렇게 편리할 수가 없다. 당장 Redis를 사용해 보자.
2. Redis 설정
1. Redis 설치
먼저 Redis를 설치해줘야 한다. 서버에 구축할 수 있지만 일단 로컬환경에 설치해 주었다. Reids 설치와 기본적인 사용방법은 이 포스팅을 참고하였다.
2. 의존성 주입 (build.gradle.kts)
설치가 완료되었다면 이제 스프링에 의존성을 주입해줘야 한다.
implementation("org.springframework.boot:spring-boot-starter-data-redis") // redis 의존성 주입
3. 설정 (. env & application.properties)
Redis의 호스트와 포트를 설정해 준다.
4. RedisConfig 클래스 생성
컨피그 파일을 생성하여 스프링 컨테이너에 빈 등록을 해주고, Redis 레포지토리 기능을 활성화해주는 등 기본적인 설정을 한다.
package kr.co.vibevillage.user.redis.config;
import lombok.RequiredArgsConstructor;
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.repository.configuration.EnableRedisRepositories;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
@RequiredArgsConstructor
@EnableRedisRepositories // Redis 레포지토리 기능 활성화
public class RedisConfig {
// RedisProperties 객체 생성
private final RedisProperties redisProperties;
@Bean // 스프링 컨테이너에 RedisConnectionFactory 빈 등록
public RedisConnectionFactory redisConnectionFactory(){
// LettuceConnectionFactory를 사용하여 Redis 연결 팩토리 생성, 호스트와 포트 정보를 사용
return new LettuceConnectionFactory(redisProperties.getHost(), redisProperties.getPort());
}
@Bean
public RedisTemplate<String, Object> redisTemplate() {
// RedisTemplate 인스턴스 생성
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
// Redis 연결 팩토리 설정
redisTemplate.setConnectionFactory(redisConnectionFactory());
// 키를 문자열로 직렬화하도록 설정
redisTemplate.setKeySerializer(new StringRedisSerializer());
// 값을 문자열로 직렬화하도록 설정
redisTemplate.setValueSerializer(new StringRedisSerializer());
// 설정이 완료된 RedisTemplate 인스턴스를 반환
return redisTemplate;
}
}
5. RedisRepository 생성
dao 파일을 생성하여 Redis에 저장, 삭제, 조회할 수 있도록 설정한다.
package kr.co.vibevillage.user.redis.dao;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Repository;
import java.time.Duration;
@Repository
@RequiredArgsConstructor
public class RedisRepository {
private final String PREFIX = "sms:"; // key값이 중복되지 않도록 상수 선언
private final int LIMIT_TIME = 3 * 60; // 인증번호 유효 시간
private final StringRedisTemplate stringRedisTemplate;
// Redis에 저장
public void createSmsCertification(String userPhone, String certificationCode) {
stringRedisTemplate.opsForValue().set(PREFIX + userPhone, certificationCode, Duration.ofSeconds(LIMIT_TIME));
}
// 휴대전화 번호에 해당하는 인증번호 불러오기
public String getSmsCertification(String userPhone) {
return stringRedisTemplate.opsForValue().get(PREFIX + userPhone);
}
// 인증 완료 시, 인증번호 Redis에서 삭제
public void deleteSmsCertification(String userPhone) {
stringRedisTemplate.delete(PREFIX + userPhone);
}
// Redis에 해당 휴대번호로 저장된 인증번호가 존재하는지 확인
public boolean hasKey(String userPhone) {
return stringRedisTemplate.hasKey(PREFIX + userPhone);
}
}
6. JavaScript 파일 작성
유저가 입력한 값과 연락처를 데이터로 전송한다.
<input id="phone" type="text" placeholder="연락처" onkeyup="phoneValidation()"/>
<button type="button" th:onclick="certification()">본인인증</button>
<div id="showPhone"></div>
<div style="display: none" id="certificationArea">
<input id="certificationInput" type="text" placeholder="인증번호"/>
<button type="button" th:onclick="startCertification()">인증하기</button>
</div>
function startCertification() {
const phone = document.getElementById("phone").value; // 유저가 입력한 연락처
const certificationInput = document.getElementById("certificationInput").value; // 유저가 입력한 인증번호
const certificationArea = document.getElementById("certificationArea"); // 인증 영역
// 인증 시작
$.ajax({
type: "POST",
url: "/certification",
data: {
certificationInput : certificationInput,
userPhone : phone,
},
success: function success(res) {
if(res === "인증실패"){
swal("인증실패", "인증번호를 확인해주세요.", "error");
} else if (res === "인증성공") {
swal("인증완료", "", "success");
certificationArea.style.display = "none"; // 인증 영역 안보이게 설정
}
},
error: function error(err) {
}
});
}
7. Controller 작성
전송받은 데이터를 받아 userPhone, certificationInput 매개변수에 저장하고 서비스 객체를 이용해 verify 메소드를 호출한다.
package kr.co.vibevillage.user.controller;
import kr.co.vibevillage.user.model.dto.UserDTO;
import kr.co.vibevillage.user.model.service.RegisterServiceImpl;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
@RequiredArgsConstructor
public class RegisterController {
... (생략)
@PostMapping("/sendCertificationNumber")
@ResponseBody
public void certification(String userPhone) {
registerService.sendSms(userPhone);
}
// 인증 시작
@PostMapping("/certification")
@ResponseBody
public String startCertification(String userPhone,String certificationInput) {
return registerService.verify(userPhone, certificationInput);
}
}
8. Service 작성
여기서 작성하던 파일과 동일한 파일이다. 문자인증 코드 하단에다가 이어서 작성해 주었다. (주석 달려있는 부분)
클래스 하단에 검증을 하는 isVerify 메소드를 작성하여 활용했다.
package kr.co.vibevillage.user.model.service;
import kr.co.vibevillage.user.model.dto.UserDTO;
import kr.co.vibevillage.user.model.mapper.RegisterMapper;
import kr.co.vibevillage.user.model.util.CertificationUtil;
import kr.co.vibevillage.user.redis.dao.RedisRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class RegisterServiceImpl implements RegisterService{
private final RegisterMapper registerMapper;
private final CertificationUtil certificationUtil;
private final RedisRepository redisRepository;
...(생략)
@Override
public void sendSms(String userPhone) {
String certificationCode = Integer.toString((int)(Math.random() * (999999 - 100000 + 1)) + 100000);
certificationUtil.sendSMS(userPhone, certificationCode);
// Redis에 key(userPhone), value(certificationCode)로 저장
redisRepository.createSmsCertification(userPhone, certificationCode);
}
// 인증 번호 검증
public String verify(String userPhone, String certificationInput) {
boolean result = isVerify(userPhone, certificationInput);
if(result) {
return "인증실패";
} else{
return "인증성공";
}
}
// 검증 메소드
private boolean isVerify(String userPhone, String certificationInput) {
return !(redisRepository.hasKey(userPhone) && redisRepository.getSmsCertification(userPhone).equals(certificationInput));
}
}
3. Redis 사용해 보기
이렇게 코드를 작성하고 인증번호 요청을 하면 요청한 연락처(userPhone)와 인증번호(certificationCode)가 저장되고 인증번호 검증을 요청했을 때 Redis 내부에 저장되어 있는 값과 유저가 입력한 값(certificationInput)을 비교하여 결과값을 리턴한다. 사진으로 함께 보자.
1. Redis 값 확인
1. brew services start redis
2. redis-cli
3. keys *
위 순서대로 입력하여 현재 Redis에 저장되어있는 값을 확인한다. 아무런 행위 없이 조회하였기에 당연히 비어있다고 나온다.
2. 인증 요청
연락처를 입력하고 본인인증 버튼을 클릭한다.
다시 조회해 보면 key값 조회가 된다.
인증요청을 하고 나서 3분(180초) 이내에 인증을 해야 하며 다른 인증코드를 입력하면 아래와 같이 alert 창이 뜬다.
정확한 인증코드를 입력하면 인증이 완료된다.
인증이 완료된 이후 데이터가 삭제되게 코드를 작성할 수 도 있고, TTL을 활용하였기에 인증 요청 이후 3분(180초) 뒤에 데이터는 알아서 삭제가 된다. 너무너무 편리하고 좋다. 이렇게 본인인증(인증코드 문자 발송 + Redis를 이용한 인증)을 완료하였다. 다음에는 Bcrypt를 이용한 비밀번호 암호화에 대해 작성해 보겠다.
< 참고한 블로그 >
1. 개발공부 메모로그 =]
2. l0o0lv.log
3. eqvyoo.log
'개발 > Team Project' 카테고리의 다른 글
ep10. 로그인 기능 구현 (feat. SpringSecurity 초기 설정 + JWT 생성) (0) | 2024.08.14 |
---|---|
ep.09 비밀번호 암호화 (Bcrypt) (0) | 2024.08.13 |
ep07. 회원가입 기능 구현 - 중복검사 (0) | 2024.08.07 |
ep.06 템플릿 선정 & 프론트 작업(유효성 검사, 프로필 사진 업로드와 미리보기, 주소 API 연동) (0) | 2024.08.02 |
ep.05 Thymeleaf 설정 & GitHub를 이용한 프로젝트 공유 (0) | 2024.07.30 |