크리스마스날 진행한 작업은 다음과 같다.
- Redis연결 및 이메일 인증 기능 구현
크리스마스날 개인과제 마무리를 하기 위해 챌린지 과제인 이메일 인증 구현을 마무리했다.
마무리한 코드를 바탕으로 동작하는 과정을 적어보려고 한다. 순서가 동작하는 과정을 중심으로 돼있기 때문에 혼동이 올 수 있다.
1. 회원가입을 위해 ToDoUserController의 회원가입 정보에 대한 SignupRequestDto를 입력해 주면 된다. username, password, email정보를 입력했다. 입력한 정보에 따라 URL에 Request를 전송하면 회원 가입 절차를 진행하며 prepareSignup 메서드를 통해 UserRepository에 임시저장을 마치면 EmailMessage를 생성하여 필요한 정보를 입력한 객체를 생성하여 요청후 Email 인증을 진행한다.
3. 2번에서 EmailService클래스에 있는 sendMail메서드를 실행하고 나서 이메일로 받은 인증코드를 통해 바로 이메일 인증을 진행하면 된다. 인증 절차를 마치면 verifyCheckFlag 체크를 통해 알맞은 Response를 반환한다.
package com.sparta.project_todo.user.controller;
import static com.sparta.project_todo.global.constant.ErrorCode.*;
import static com.sparta.project_todo.global.constant.ResponseCode.*;
import java.util.List;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
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.sparta.project_todo.email.entity.EmailMessage;
import com.sparta.project_todo.email.service.EmailService;
import com.sparta.project_todo.global.dto.ErrorResponse;
import com.sparta.project_todo.global.dto.SuccessResponse;
import com.sparta.project_todo.user.dto.SignupRequestDto;
import com.sparta.project_todo.user.dto.VerificationRequestDto;
import com.sparta.project_todo.user.service.UserService;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@RestController
@RequestMapping("/api")
public class ToDoUserController {
private final UserService userService;
private final EmailService emailService;
public ToDoUserController(UserService userService, EmailService emailService) {
this.userService = userService;
this.emailService = emailService;
}
// 회원가입
@PostMapping("/user/signup")
public ResponseEntity<?> signup(@Valid @RequestBody SignupRequestDto requestDto, BindingResult bindingResult) {
log.info(requestDto.toString());
// Validation 예외처리
List<FieldError> fieldErrors = bindingResult.getFieldErrors();
if (fieldErrors.size() > 0) {
for (FieldError fieldError : bindingResult.getFieldErrors()) {
log.error(fieldError.getField() + " 필드 : " + fieldError.getDefaultMessage());
}
return ResponseEntity.ok("회원 가입 실패");
}
userService.prepareSignup(requestDto);
EmailMessage emailMessage = EmailMessage.builder()
.to(requestDto.getEmail())
.subject("[SAVIEW] 이메일 인증을 위한 인증 코드 발송")
.build();
emailService.sendMail(emailMessage, "email");
return ResponseEntity.status(HttpStatus.OK.value())
.body(new SuccessResponse(HttpStatus.OK.value(), "이메일을 통해 인증코드를 보냈습니다 5분안에 인증을 진행해 주세요"));
}
// 이메일 인증
@PostMapping("/user/email-code/verification")
public ResponseEntity<?> verifyAuthCode(@RequestBody VerificationRequestDto verificationRequestDto){
Boolean verifyCheckFlag = userService.verifyAuthCode(verificationRequestDto);
if( verifyCheckFlag == null){
return ResponseEntity.status(INVALID_SIGNUP.getHttpStatus().value())
.body(new ErrorResponse(INVALID_SIGNUP.getHttpStatus().value(), INVALID_SIGNUP.getMessage() ));
} else if(verifyCheckFlag) {
return ResponseEntity.status(SUCCESS_SIGNUP.getHttpStatus().value())
.body(new SuccessResponse(SUCCESS_SIGNUP));
} else{
return ResponseEntity.status(INVALID_SIGNUP.getHttpStatus().value())
.body(new ErrorResponse(INVALID_SIGNUP.getHttpStatus().value(), INVALID_SIGNUP.getMessage()+ "다시 인증해주세요"));
}
}
}
package com.sparta.project_todo.email.entity;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Builder
public class EmailMessage {
private String to;
private String subject;
private String message;
}
package com.sparta.project_todo.user.dto;
import lombok.Getter;
@Getter
public class VerificationRequestDto {
private String email;
private String authCode;
}
2. 이메일 인증을 구현하기 위해서는 JavaMailSender, SpringTemlateEngine을 통해 구현한다.
EmailService는 Email 인증요청에 대한 로직을 수행하는 클래스이다. UserController에서 signup 요청을 통해 들어온 email을 바탕으로 이메일 전송을 위한 서비스를 실행한다.
SendEmail 메서드는 EmailMessage에서 받은 정보를 통해 createCode메서드에서 생성한 인증코드와 email 정보를 Spring Framework에서 제공하는 MimeMessageHelper의 클래스를 통해 객체를 생성한다. 객체의 정보입력 후 메일을 발송하고 redis에도 5분 동안 유지되는 key값인 email 정보와 value값인 생성한 인증코드를 저장한다. 여기까지 진행하면 이메일을 전송하는 과정이 마무리된다.
package com.sparta.project_todo.email.service;
import java.util.Random;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Service;
import org.thymeleaf.context.Context;
import org.thymeleaf.spring6.SpringTemplateEngine;
import com.sparta.project_todo.email.entity.EmailMessage;
import com.sparta.project_todo.redis.util.RedisUtil;
import com.sparta.project_todo.user.service.UserService;
import jakarta.mail.MessagingException;
import jakarta.mail.internet.MimeMessage;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Service
@RequiredArgsConstructor
public class EmailService {
private final JavaMailSender javaMailSender;
private final SpringTemplateEngine templateEngine;
private final RedisUtil redisUtil;
public void sendMail(EmailMessage emailMessage, String type) {
String authNum = createCode();
MimeMessage mimeMessage = javaMailSender.createMimeMessage();
try {
MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(mimeMessage, false, "UTF-8");
mimeMessageHelper.setTo(emailMessage.getTo()); // 메일 수신자
mimeMessageHelper.setSubject(emailMessage.getSubject()); // 메일 제목
mimeMessageHelper.setText(setContext(authNum, type), true); // 메일 본문 내용, HTML 여부
javaMailSender.send(mimeMessage);
redisUtil.setDataExpire(emailMessage.getTo(),authNum, 60*5L);
log.info("Success");
} catch (MessagingException e) {
log.info("fail");
throw new RuntimeException(e);
}
}
// 인증번호 생성
public String createCode() {
Random random = new Random();
StringBuffer key = new StringBuffer();
for (int i = 0; i < 8; i++) {
int index = random.nextInt(4);
switch (index) {
case 0: key.append((char) ((int) random.nextInt(26) + 97)); break;
case 1: key.append((char) ((int) random.nextInt(26) + 65)); break;
default: key.append(random.nextInt(9));
}
}
return key.toString();
}
// thymeleaf를 통한 html 적용
public String setContext(String code, String type) {
Context context = new Context();
context.setVariable("code", code);
return templateEngine.process(type, context);
}
public Boolean verifyEmailCode(String email, String code){
String findCodeByEmail = redisUtil.getData(email);
if(findCodeByEmail == null){
return null;
} else{
return findCodeByEmail.equals(code);
}
}
}
4. 3번에서 실행한 verifyAuthCode를 통해 UserService에서 인증 절차를 진행하며 EmailService 클래스에 있는 verifyEmailCode메서드를 통해 입력받은 email과 인증코드를 통해 redis에 key값으로 저장되어 있는 email정보에 따른 인증코드 value 값과 입력받은 인증코드값이 같은지 확인하고 같으면 true 같지 않으면 false 찾을 수 없다면 null 값을 반환한다. 체크 여부에 따라 5번으로 넘어간다.
package com.sparta.project_todo.user.service;
import java.util.Optional;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import com.sparta.project_todo.email.service.EmailService;
import com.sparta.project_todo.security.JwtUtil;
import com.sparta.project_todo.user.dto.SignupRequestDto;
import com.sparta.project_todo.user.dto.VerificationRequestDto;
import com.sparta.project_todo.user.entity.User;
import com.sparta.project_todo.user.entity.UserRoleEnum;
import com.sparta.project_todo.user.repository.UserRepository;
import lombok.RequiredArgsConstructor;
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final EmailService emailService;
private final JwtUtil jwtUtil;
public void prepareSignup(SignupRequestDto requestDto) {
String username = requestDto.getUsername();
// 회원 중복 확인
Optional<User> checkUsername = userRepository.findByUsername(username);
if (checkUsername.isPresent()) {
throw new IllegalArgumentException("중복된 사용자가 존재합니다.");
}
// email 중복확인
String email = requestDto.getEmail();
Optional<User> checkEmail = userRepository.findByEmail(email);
if (checkEmail.isPresent()) {
throw new IllegalArgumentException("중복된 Email 입니다.");
}
String password = passwordEncoder.encode(requestDto.getPassword());
// 사용자 ROLE 확인
UserRoleEnum role = UserRoleEnum.USER;
// 사용자 임시 등록
User user = new User(username, password, email, role);
userRepository.save(user);
}
// 인증절차 진행
public Boolean verifyAuthCode(VerificationRequestDto verificationRequestDto){
String email = verificationRequestDto.getEmail();
String code = verificationRequestDto.getAuthCode();
Boolean verifyCheckFlag = emailService.verifyEmailCode(email,code);
verifyCheck(verifyCheckFlag, verificationRequestDto.getEmail());
return verifyCheckFlag;
}
// 인증 Flag에 따라 회원가입 여부 결정
public void verifyCheck(Boolean verifyCheckFlag , String email){
if(verifyCheckFlag == null){
userRepository.failVerify(email);
} else if(verifyCheckFlag){
userRepository.successVerify(email);
}
}
}
public Boolean verifyEmailCode(String email, String code){
String findCodeByEmail = redisUtil.getData(email);
if(findCodeByEmail == null){
return null;
} else{
return findCodeByEmail.equals(code);
}
}
5. 체크 여부에 따라 성공하면 UserRepository에 JPQL로 바로 접근하여 emailCheck칼럼을 true값으로 변경하여 회원가입 처리를 완료한다. 실패한다면 redis에 유효시간이 있는 정보가 아직 있다면 재인증 시도를 얼마든지 할 수 있으며 만약 redis에 저장된 5분이 만료되어 정보가 사라지면 null 값이 반환되며 재인증시 회원가입 실패 처리와 함께 임시로 저장한 User의 정보를 삭제한다.
// 회원가입 성공시 emailCheck true로 변경하여 회원가입 처리 완료
@Transactional
@Modifying
@Query("update User u set u.emailCheck = true where u.email = :email and u.emailCheck = false ")
void successVerify(String email);
// 회원가입 실패시 임시로 저장한 회원정보 삭제
@Transactional
@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("delete from User u where u.email = :email and u.emailCheck = false")
void failVerify(String email);
6. 여기까지 하면 모든 인증과정 및 회원가입 처리가 마무리되며 위 코드를 설명하면서 설명하지 못한 Redis설정에 대해서 아래 입력하려고 한다.
Redis설정은 다음과 같다.
apllication.yaml
spring:
data:
redis:
host: ${REDIS_HOST} #redis Host정보 보통 localhost
port: ${REDIS_PORT} #redis Port정보 보통 6379
RedisConfig
package com.sparta.project_todo.redis.config;
import org.springframework.beans.factory.annotation.Value;
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;
@Configuration
@EnableRedisRepositories
public class RedisConfig {
@Value("${spring.data.redis.host}")
private String redisHost;
@Value("${spring.data.redis.port}")
private int redisPort;
@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(redisHost, redisPort);
}
@Bean
public RedisTemplate<?, ?> redisTemplate() {
RedisTemplate<byte[], byte[]> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory((redisConnectionFactory()));
return redisTemplate;
}
}
RedisUtil
package com.sparta.project_todo.redis.util;
import java.time.Duration;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Service
public class RedisUtil {
private final StringRedisTemplate template;
public String getData(String key) {
ValueOperations<String, String> valueOperations = template.opsForValue();
return valueOperations.get(key);
}
public boolean existData(String key) {
return Boolean.TRUE.equals(template.hasKey(key));
}
public void setDataExpire(String key, String value, long duration) {
ValueOperations<String, String> valueOperations = template.opsForValue();
Duration expireDuration = Duration.ofSeconds(duration);
valueOperations.set(key, value, expireDuration);
}
public void deleteData(String key) {
template.delete(key);
}
}
다음으로는 Email인증 html이다 resources->templates
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<div style="margin:100px;">
<h1> 안녕하세요.</h1>
<h1> 안녕하세요 회원가입 인증을 위한 인증번호 입니다.</h1>
<br>
<p> 아래 코드를 회원가입 창으로 돌아가 입력해주세요.</p>
<br>
<div align="center" style="border:1px solid black; font-family:verdana;">
<h3 style="color:blue"> 회원가입 인증 코드 입니다. </h3>
<div style="font-size:130%" th:text="${code}"> </div>
</div>
<br/>
</div>
</body>
</html>
다음으로는 User의 Entity와 UserRepository이다.
User
package com.sparta.project_todo.user.entity;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Entity
@Getter
@Setter
@NoArgsConstructor
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id; // 유저 id
@Column(nullable = false, unique = true)
private String username; // ID
@Column(nullable = false)
private String password; // PW
@Column(nullable = false, unique = true)
private String email; // email
@Column(nullable = false)
private boolean emailCheck = false; // 이메일 인증
@Column(nullable = false)
@Enumerated(value = EnumType.STRING)// 이넘 사용시 데이터 저장 어노테이션
private UserRoleEnum role; // 유저 권한 정보
public User(String username, String password, String email ,UserRoleEnum role) {
this.username = username;
this.password = password;
this.email = email;
this.role = role;
}
}
UserRepository
package com.sparta.project_todo.user.repository;
import com.sparta.project_todo.user.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.transaction.annotation.Transactional;
import java.util.Optional;
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsername(String userName); // 같은 유저 이름 찾기
Optional<User> findByEmail(String email);
@Transactional
@Modifying
@Query("update User u set u.emailCheck = true where u.email = :email and u.emailCheck = false ")
void successVerify(String email);
@Transactional
@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("delete from User u where u.email = :email and u.emailCheck = false")
void failVerify(String email);
}
7. 실행 과정
signup
이메일 인증
'내일 배움 캠프' 카테고리의 다른 글
2023-12-27 (0) | 2023.12.27 |
---|---|
2023-12-26 (0) | 2023.12.27 |
2023-12-22 (0) | 2023.12.24 |
2023-12-21 (1) | 2023.12.22 |
2023-12-20 (0) | 2023.12.20 |