Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |
Tags
- 코딩테스트 준비
- 파이썬
- 인프콘 2024
- 1주일회고
- TiL
- @FeignClient
- 개발자 취업
- 항해99
- 빈 조회 2개 이상
- 디자인패턴
- JavaScript
- jwt
- 프로그래머스
- 단기개발자코스
- jwttoken
- infcon 2024
- 프로그래머스 이중우선순위큐
- 취업리부트코스
- 99클럽
- 구글 OAuth login
- 전략패턴 #StrategyPattern #디자인패턴
- 개발자부트캠프추천
- Python
- spring batch 5.0
- 커스텀 헤더
- KPT회고
- 빈 충돌
- DesignPattern
- Spring multimodule
- 디자인 패턴
Archives
- Today
- Total
m1ndy5's coding blog
[SpringBoot 3.0] 이메일 인증을 통한 회원가입 기능 만들기 본문
Toy Projects/Ditto - Discuss Today's Topic
[SpringBoot 3.0] 이메일 인증을 통한 회원가입 기능 만들기
정민됴 2024. 1. 24. 23:43build.gradle
// email
implementation 'org.springframework.boot:spring-boot-starter-mail'
// string auto generate
implementation 'org.apache.commons:commons-lang3:3.12.0'
//thymeleaf
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect'
위와 같은 디펜던시를 추가해주었다.
순서대로 이메일을 보내기 위해, 랜덤생성코드를 위해, 이메일을 좀 더 예쁘게 보내기 위해 필요한 디펜던시들이다.
일단 이메일을 보내려면 보내는 사람의 아이디와 앱 비밀번호를 설정하는 것이 필요한데, 이 방법은.. 인터넷에 검색하면 잘나와있다..ㅎㅎ
application.yml
mail:
host: smtp.gmail.com
port: 587
username: ${MAIL_USERNAME}
password: ${MAIL_PASSWORD}
나는 구글을 사용해서 했는데 username과 password는 따로 빼주었다!
MemberAuthenticationCodeEntity
인증번호를 저장해야 비교할 수 있기 때문에 인증번호를 저장해줄 엔티티를 만들었다.
package hanghae99.ditto.auth.domain;
import hanghae99.ditto.global.entity.BaseEntity;
import hanghae99.ditto.global.entity.UsageStatus;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
@Getter
@Entity
@NoArgsConstructor
public class MemberAuthenticationCodeEntity extends BaseEntity {
@Column(name = "email", nullable = false)
private String email;
@Column(name = "code", unique = true, nullable = false)
private String code;
@Column(name = "end_date", nullable = false)
private LocalDateTime endDate;
public MemberAuthenticationCodeEntity(String email, String code){
this.email = email;
this.code = code;
this.endDate = LocalDateTime.now().plus(5, ChronoUnit.MINUTES);
this.status = UsageStatus.ACTIVE;
}
public void deleteCode(){
this.status = UsageStatus.DELETED;
}
}
이메일, 인증코드, 만료시점을 담았고 status는 생성됐을 때는 ACTIVE 지워졌을 때는 DELETED가 들어올 수 있게 했다.
MemberAuthenticationCodeRepository
package hanghae99.ditto.auth.domain;
import hanghae99.ditto.global.entity.UsageStatus;
import org.springframework.data.jpa.repository.JpaRepository;
import java.time.LocalDateTime;
import java.util.Optional;
public interface MemberAuthenticationCodeRepository extends JpaRepository<MemberAuthenticationCodeEntity, Long> {
// 이메일로 end_date가 지금 이후고, status가 active인거 찾아오기
// 즉, 아직 유효한 인증코드 불러오기
Optional<MemberAuthenticationCodeEntity> findByEmailAndEndDateAfterAndStatusEquals(String email, LocalDateTime currentDateTime, UsageStatus usageStatus);
}
이메일로 코드를 찾을 건데 만료가 되기 전인 코드만 찾을 것이다! 비교할 때 사용할 것이다.
AuthController
package hanghae99.ditto.auth.controller;
import hanghae99.ditto.auth.dto.request.AuthenticateCodeRequest;
import hanghae99.ditto.auth.dto.request.LoginRequest;
import hanghae99.ditto.auth.dto.request.LogoutRequest;
import hanghae99.ditto.auth.dto.request.SendEmailAuthenticationRequest;
import hanghae99.ditto.auth.service.AuthService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/auth")
public class AuthController {
private final AuthService authService;
@PostMapping("/email-authentication")
public HttpEntity<?> sendEmailAuthentication(@RequestBody SendEmailAuthenticationRequest sendEmailAuthenticationRequest){
return authService.sendEmailAuthentication(sendEmailAuthenticationRequest);
}
@PostMapping("/authentication-code")
public HttpEntity<?> authenticateCode(@RequestBody AuthenticateCodeRequest authenticateCodeRequest){
return authService.authenticateCode(authenticateCodeRequest);
}
}
SendEmailAuthenticationRequest
package hanghae99.ditto.auth.dto.request;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class SendEmailAuthenticationRequest {
@Pattern(regexp = "^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+.[A-Za-z]{2,6}$", message = "이메일 형식이 올바르지 않습니다.")
@NotBlank(message = "이메일은 필수 입력 값입니다.")
private String email;
}
인증코드를 받을 이메일을 보내는 DTO이다.
EmailAuthenticationResponse
package hanghae99.ditto.auth.dto.response;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class EmailAuthenticationResponse {
private Integer code;
private String message;
}
code는 성공여부를 나타내고 message에 성공실패 여부가 들어갈 것이다.
AuthenticateCodeRequest
package hanghae99.ditto.auth.dto.request;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class AuthenticateCodeRequest {
@Pattern(regexp = "^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+.[A-Za-z]{2,6}$", message = "이메일 형식이 올바르지 않습니다.")
@NotBlank(message = "이메일은 필수 입력 값입니다.")
private String email;
@NotBlank(message = "인증코드는 필수 입력 값입니다.")
@Size(max = 10)
private String code;
}
인증코드를 확인하는 DTO이다.
EmailService
package hanghae99.ditto.auth.service;
import hanghae99.ditto.auth.dto.request.SendEmailAuthenticationRequest;
import jakarta.mail.Message;
import jakarta.mail.MessagingException;
import jakarta.mail.internet.InternetAddress;
import jakarta.mail.internet.MimeMessage;
import lombok.RequiredArgsConstructor;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;
import java.io.UnsupportedEncodingException;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class EmailService {
private final JavaMailSender javaMailSender;
private final TemplateEngine templateEngine;
public Boolean sendEmailAuthentication(SendEmailAuthenticationRequest sendEmailAuthenticationRequest,
String authenticationCode){
MimeMessage message = javaMailSender.createMimeMessage();
try {
// 이메일 제목 설정
message.setSubject("사이트 회원가입 인증번호 입니다.");
// 이메일 수신자 설정
message.addRecipients(Message.RecipientType.TO, String.valueOf(new InternetAddress(sendEmailAuthenticationRequest.getEmail(), "", "UTF-8")));
// 이메일 내용 설정
message.setText(setContext(authenticationCode), "UTF-8", "html");
// 송신
javaMailSender.send(message);
} catch (MessagingException e) {
return false;
} catch (UnsupportedEncodingException e) {
return false;
}
return true;
}
private String setContext(String authenticationCode){
Context context = new Context();
context.setVariable("authenticationCode", authenticationCode);
return templateEngine.process("email-authentication", context);
}
}
이메일을 보내는 서비스이다.
AuthService
package hanghae99.ditto.auth.service;
import hanghae99.ditto.auth.domain.MemberAuthenticationCodeEntity;
import hanghae99.ditto.auth.domain.MemberAuthenticationCodeRepository;
import hanghae99.ditto.auth.dto.request.AuthenticateCodeRequest;
import hanghae99.ditto.auth.dto.request.LoginRequest;
import hanghae99.ditto.auth.dto.request.LogoutRequest;
import hanghae99.ditto.auth.dto.request.SendEmailAuthenticationRequest;
import hanghae99.ditto.auth.dto.response.EmailAuthenticationResponse;
import hanghae99.ditto.auth.dto.response.LoginResponse;
import hanghae99.ditto.auth.support.JwtTokenProvider;
import hanghae99.ditto.auth.support.RedisUtil;
import hanghae99.ditto.global.entity.UsageStatus;
import hanghae99.ditto.member.domain.Member;
import hanghae99.ditto.member.domain.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.RandomStringUtils;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.Optional;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class AuthService {
private final MemberAuthenticationCodeRepository memberAuthenticationCodeRepository;
private final MemberRepository memberRepository;
private final EmailService emailService;
private final BCryptPasswordEncoder bCryptPasswordEncoder;
private final JwtTokenProvider jwtTokenProvider;
private final RedisUtil redisUtil;
@Transactional
public HttpEntity<?> sendEmailAuthentication(SendEmailAuthenticationRequest sendEmailAuthenticationRequest){
// 랜덤 인증 코드 생성
String authenticationCode = createAuthenticationCode();
// emailSerivce의 인증코드 보내는 메서드의 성공 여부에 따라 응답
if (!emailService.sendEmailAuthentication(sendEmailAuthenticationRequest, authenticationCode)){
return new ResponseEntity<>(
new EmailAuthenticationResponse(-1, "인증 번호 발송 실패"), HttpStatus.BAD_REQUEST
);
}
// 메일 발송 성공 시
// 아직 유효한 인증 코드 데이터를 찾아서
Optional<MemberAuthenticationCodeEntity> beforeMemberAuthenticationCodeEntityOptional = memberAuthenticationCodeRepository
.findByEmailAndEndDateAfterAndStatusEquals(
sendEmailAuthenticationRequest.getEmail(),
LocalDateTime.now(),
UsageStatus.ACTIVE);
// 있으면 무효화 (delete_date 설정)
if (beforeMemberAuthenticationCodeEntityOptional.isPresent()){
MemberAuthenticationCodeEntity beforeMemberAuthenticationCodeEntity = beforeMemberAuthenticationCodeEntityOptional.get();
beforeMemberAuthenticationCodeEntity.deleteCode();
}
// 인증 코드 데이터를 저장하기 위해 새 엔티티를 작성하여
MemberAuthenticationCodeEntity memberAuthenticationCodeEntity = new MemberAuthenticationCodeEntity(sendEmailAuthenticationRequest.getEmail(), authenticationCode);
memberAuthenticationCodeRepository.save(memberAuthenticationCodeEntity);
return new ResponseEntity<>(
new EmailAuthenticationResponse(0, "인증 번호 발송 성공"), HttpStatus.OK
);
}
public String createAuthenticationCode(){
return RandomStringUtils.random(8, true, true);
}
@Transactional
public HttpEntity<?> authenticateCode(AuthenticateCodeRequest authenticateCodeRequest){
// 유효한 인증 코드 데이터를 찾아서
Optional<MemberAuthenticationCodeEntity> memberAuthenticationCodeEntityOptional =
memberAuthenticationCodeRepository.findByEmailAndEndDateAfterAndStatusEquals(authenticateCodeRequest.getEmail(), LocalDateTime.now(), UsageStatus.ACTIVE);
// 없으면 인증 코드 없음 반환
if (memberAuthenticationCodeEntityOptional.isEmpty()){
return new ResponseEntity<>(
new EmailAuthenticationResponse(-1, "인증 코드 없음"), HttpStatus.BAD_REQUEST
);
}
// 있으면 찾아서
MemberAuthenticationCodeEntity memberAuthenticationCodeEntity = memberAuthenticationCodeEntityOptional.get();
// 입력한 인증 코드가 일치하는 지 검증
if (memberAuthenticationCodeEntity.getCode().equals(authenticateCodeRequest.getCode())){
memberAuthenticationCodeEntity.deleteCode();
// 해당 멤버 status 인증됨으로 변경
Member member = memberRepository.findByEmail(authenticateCodeRequest.getEmail()).orElseThrow(() -> {
throw new IllegalArgumentException("유효하지 않은 이메일입니다.");
});
member.verifiedWithEmail();
return new ResponseEntity<>(
new EmailAuthenticationResponse(0, "인증 성공"), HttpStatus.OK
);
} else {
return new ResponseEntity<>(
new EmailAuthenticationResponse(-1, "인증 실패"), HttpStatus.BAD_REQUEST
);
}
}
}
이메일 인증의 제일 핵심로직이라고 할 수 있다.
로직의 동작 원리는
- 랜덤 인증 코드를 생성한다.
- 인증코드를 보낸다. -> 실패시 실패 응답 전송
- 성공시 같은 메일로 받은 기존의 코드가 있는지 확인한다. -> 있으면 그 코드는 없앤다.
- 새로운 인증코드를 저장한다.
이제 가입자 입장에서는 인증코드를 받아서 이거 맞냐?하고 보내면
- 이 이메일로 받은 인증코드가 있는지 확인 -> 없으면 인증코드없다고 응답
- 있다면 일치하는지 확인 -> 일치하지 않는다면 일치하지 않는다고 응답
- 인증 완료되면 멤버의 status를 active로 바꿔준다.
진짜 참고한 블로거분이 진짜 정리를 너무 잘해놓으셔서 아래를 보고 차근차근 따라가시길!
'Toy Projects > Ditto - Discuss Today's Topic' 카테고리의 다른 글
Ditto 프로젝트 멀티 모듈 도입 3편 - Gateway 도입하기 (2) | 2024.02.06 |
---|---|
Ditto 프로젝트 멀티 모듈 도입 2편 - Entity 연관관계 끊기 (0) | 2024.02.06 |
Ditto 프로젝트 멀티 모듈 도입 1편 - 모듈 나누기에 대한 고민 (1) | 2024.02.02 |
DDD? MSA? 멀티 모듈? (0) | 2024.01.31 |
[SpringBoot 3.0] JWT로 로그아웃을???(feat. Redis) (2) | 2024.01.25 |