프로젝트를 진행하면서 점점 기능을 구현하다보니 처음에 작업해놓은 기획안은 있지만,
틀만 만들고 어떻게 구현할지는 구현하면서 생각해보자 하고 만든 형태다 보니, 구체적인 설계도가 필요해졌다.
물론, 전체적으로 다시 기획안부터 만들어도 좋은 방법이지만, 해당 기능을 구현하려고 들어갈때마다, 어떻게 할지 고민하고 배워서 적용해봤기 때문에 다 만들어 보는 방법보단 조각조각 하나하나 퍼즐처럼 만들어서 마지막에 합쳐보고자 한다.
이미 작업했던 기능들을 적을까하지만, 현재 진행중인 기능을 적는게 좋겠다 생각하[현재기능 > 이미 구현한 기능] 이번 포스팅에선 자체로그인을 구현하는 로직을 풀어보려고 한다.
이번 프로젝트에서 구현할 자체로그인은
이메일
과비밀번호
만 있으면 회원가입이 되는 프로젝트이다.
그렇기때문에, 이메일을 통해 인증하는 과정이 필요하고, 아이디찾기 없이 비밀번호 찾기만 구현할 예정이다.
비밀번호 찾는 방법은 새로운 비밀번호로 변경하는 방식으로만 진행할 것이며, 해당 방법 또한 이메일로만 다 해결할 수 있게 구현하면된다.
1️⃣ 로그인 로직
2️⃣ 회원가입 로직
3️⃣ 비밀번호 찾기 로직
4️⃣ 비밀번호 바꾸기 로직
로그인이 되는지 안되는지 체크해 주면된다.
로그인이 되는 경우라면, 원래 로직대로 응답해주면된다.(로그인 유지를 위해 jwt로 응답한다.)
로그인이 안되는 경우라면, 해당 에러를 잡아 핸들링하여 응답해주면 된다.
사용자 로그인을 유지하기위해 여러 방법이 있지만, 나는 트래픽 부담이 적은 JWT방식으로 채택했다.
본인인증을 위한 과정중에 이메일만 사용하기 때문에,
이메일 인증
이 필수로 필요하다.
인증을 하기위해선인증코드를 서버에서 보관
해야되기 때문에 다양한 방법중에 만료시간(TTL)을 관리하기 좋고 동시성 덕에 제일 확장성이 좋고 빠른Redis를 선택
하였다.
비밀번호 찾기는 해당 이메일이 자체 회원가입한 회원 이메일인지 먼저 확인한 후, 있다면 이메일로 비밀번호 수정할 수 있는 Uri가 담긴 이메일을 전송해준다.
전체적인 아키텍처를 그려보면 아래와 같다.
물론 다 알고 있는 상태에서 보면 그닥 어려운 내용은 아니니, 하나하나 파헤쳐보면서 구현해보자.
구현 방식은 체크리스트와 다르게, 처음 사용하는 사용자 입장 순서대로 진행해보고자 한다.
먼저 회원가입 로직이다. 입력값 검증의 경우, 매 요청마다 처리하기때문에 N이라는 번호로 넣어줬고, 사실상 예외 핸들링 처리도 다 포함하고 있다고 보면 된다.
기본적으로 해당 환경은 DB와 서버는 환경설정이 되어 있는 상태라고 본다.
서버 : Spring boot 3.x.x
DB : MySQL
SMTP
와 Redis
설정이다. service@gloom.com
으로 이메일을 보낼 수 있는 경우를 말한다.나는 무료로 사용할 수 있고 국내에서 가장 많이 이용되는 네이버를 사용했다.
사용함
으로 체크하면 된다. 이제 SpringBoot에서 네이버 SMTP에 접근하면 된다.
//mail
implementation 'org.springframework.boot:spring-boot-starter-mail'
mail:
naver:
password: 네이버메일 비밀번호
@Configuration
public class MailConfig {
@Value("${mail.naver.password}")
private String password;
@Bean
public JavaMailSender javaMailSender() {
JavaMailSenderImpl mailSender = new JavaMailSenderImpl();
mailSender.setHost("smtp.naver.com");
mailSender.setUsername("위에 가려진 계정정보 아이디");
mailSender.setPort(465);
mailSender.setPassword(password);
mailSender.setJavaMailProperties(getMailProperties());
return mailSender;
}
// java메일 API를 사용하기 위한 다양한 속성 설정 application.yml에서 해도 됨
private Properties getMailProperties() {
Properties properties = new Properties();
properties.setProperty("mail.transport.protocol", "smtp"); // 필수
properties.setProperty("mail.smtp.auth", "true"); // 인증 필수
properties.setProperty("mail.smtp.starttls.enable", "true"); //보안 필수
properties.setProperty("mail.debug", "true"); // 선택
properties.setProperty("mail.smtp.ssl.trust","smtp.naver.com"); //필수
properties.setProperty("mail.smtp.ssl.enable","true"); // 보안 필수
return properties;
}
}
@Slf4j // 로그 찍어보기
@Service
@RequiredArgsConstructor
public class MailService {
private final JavaMailSender javaMailSender;
// 보낼 이메일 만드는 서비스
public MimeMessage createCodeEmail(String email, String code) throws Exception {
MimeMessage message = javaMailSender.createMimeMessage();
message.addRecipients(Message.RecipientType.TO, email);
message.setSubject("인증 번호입니다.");
message.setText("이메일 인증코드 : " + code);
message.setFrom("보내는사람의 이메일");
return message;
}
// 이메일 보내는 서비스
public void sendMail(MimeMessage email) throws Exception {
try {
log.info("이메일 전송 중");
javaMailSender.send(email);
} catch (MailException mailException) {
mailException.printStackTrace();
throw new IllegalAccessException();
}
}
// 인증 번호 만드는 서비스
private String createAuthNumber() {
return UUID.randomUUID().toString().substring(0, 6);
//랜덤 인증번호 uuid를 이용!
}
// 위 메소드를 다 활용한 메소드 Controller에서는 이 메소드만 사용하면됨.
public String sendCertificationMail(String email) throws Exception {
String code = createAuthNumber();
sendMail(createCodeEmail(email, code));
return code;
}
}
@PostMapping("/email-code/request")
public String mailAuthentication(@Valid @RequestBody EmailAuthenticationDto emailReq) throws Exception {
return mailService.sendCertificationMail(emailReq.getEmail());
}
redis:
host: localhost
port: 6379
@Component
@RequiredArgsConstructor
public class RedisUtil {
private final StringRedisTemplate stringRedisTemplate;
// Redis에 데이터 저장
public void setData(String key, String value) {
stringRedisTemplate.opsForValue().set(key, value);
}
// 데이터 조회
public String getData(String key) {
return stringRedisTemplate.opsForValue().get(key);
}
// 데이터 삭제
public void deleteData(String key) {
stringRedisTemplate.delete(key);
}
// 데이터 기한과 함께 저장
public void setDataExpire(String key, String value, long time) {
stringRedisTemplate.opsForValue().set(key, value, time, java.util.concurrent.TimeUnit.SECONDS); // 초단위로 계산
}
}
@Slf4j
@Service
@RequiredArgsConstructor
public class MailService {
// .. 기존 내용 생략
private final RedisUtil redisUtil; //추가
// ..중략
public String sendCertificationMail(String email) throws Exception {
String code = createAuthNumber();
//메일 생성
sendMail(createCodeEmail(email, code));
saveRedisAuthNumber(email, code, 180L); //3분
return code;
}
public String verifyEmailCode(String email, String code) {
String redisCode = redisUtil.getData(email);
if (redisCode == null) {
return "인증번호가 만료되었습니다.";
}
if (redisCode.equals(code)) {
redisUtil.deleteData(email);
return "인증 성공";
}
return "인증번호가 틀렸습니다.";
}
@PostMapping("/email-code/verify")
public String verifyEmailCode(@Valid EmailCodeVerifyDto dto) {
return mailService.verifyEmailCode(dto.getEmail(), dto.getCode());
}
내용이 좀 길어져 구현부분은 회원가입부분만 적었는데, 사실 회원가입을 구현했다면 나머지도 금방 구현할 수 있다. 다만, Mail부분과 로그인 회원가입 기능부분을 나눠서 정리하고 싶다보니까 다음 포스팅에서는 폴더 및 파일 구조에 대해서도 언급되면서 나머지 기능을 정리할 것 같다.
확실히 정리하고 나니, 반복되는 부분이나 정리해야되는 부분이 생기는 것같다.