일단 나는 자바 개발자이다. 지금부터 올리는 코드는 거의 자바로 서버를 만들고 React로 화면을 만들것이다.
일단 앱이든 웹이든 이메일 인증 같은거 있으면 나름 있어보이는 프로젝트 같은 느낌이 들게한다.
구글 이메일로 인증을하면 뭔가 구글과 관련있어 보이는거같고 구글과 협업하는거같고 그렇다.
그래서 이메일인증을 첫번째로 하려고한다.
환경
Windows
IntelliJ (30일무료판)
Vscode
JAVA openjdk 17
STS(스프링부트) :3.3.5
mysql8
node -v22.11.0
npm - 10.9.0
구글 이메일 서비스 (SMTP)
redis
구글에 이메일을 보내기 위해서는 SMTP 서비스를 이용한다.
SMTP는 대충 인터넷은 통해 이메일을 주고받을때 사용하는기능이다.
개인이 만들면 많이 힘들어지니까 구글이나 네이버에서 제공되는 SMTP기능을 사용한다.
설정방법
1. 구글에서 로그인해서 Gmail로 들어간다.
2. 오른쪽위 톱니바퀴 모양의 버튼을 클릭한다.
3. 모든설정ㄹ보기 클릭한다.
4. 탭에서 전달 및 POP/IMAP를 클릭한다.
5. 이렇게 설정한다.
이제 구글 비밀번호 설정이 필요하다.
6. 메뉴같은 점많은거 클릭후 계정 클릭한다.
7. 왼쪽 보안- 2단계 인증 클릭
8. 앱비밀번호 설정이 있는데 설정해서 비밀번호 발급받아서 어디 저장한다. 나중에 Spring boot 에 쓸예정
이제 설정은 끝낫다
이렇게 따라서 만드세요.
백엔드
build.gradle
plugins {
id 'java'
id 'org.springframework.boot' version '3.3.5'
id 'io.spring.dependency-management' version '1.1.6'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
repositories {
mavenCentral()
}
dependencies {
// Spring Boot의 기본 스타터 의존성
implementation 'org.springframework.boot:spring-boot-starter'
implementation 'org.springframework.boot:spring-boot-starter-mail' // JavaMailSender 의존성 추가
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' // Thymeleaf 템플릿 엔진 통합 의존성
// 테스트 관련 의존성
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
// Lombok 설정 (컴파일 타임에만 Lombok 사용)
compileOnly 'org.projectlombok:lombok:1.18.26'
annotationProcessor 'org.projectlombok:lombok:1.18.26'
// JPA 관련 의존성
implementation 'jakarta.persistence:jakarta.persistence-api:3.1.0' // 최신 버전 확인 필요
implementation 'org.hibernate:hibernate-core:6.0.0.Final' // Hibernate JPA 구현체 사용
// Redis 관련 의존성
implementation 'org.springframework.boot:spring-boot-starter-data-redis' // Spring Boot Redis Starter
implementation 'org.springframework.data:spring-data-redis' // Spring Data Redis
// 웹 관련 의존성
implementation 'org.springframework.boot:spring-boot-starter-web' // Spring Boot Web Starter
implementation 'org.springframework.boot:spring-boot-starter-data-jpa' // Spring Data JPA Starter
implementation 'org.springframework.boot:spring-boot-starter-jdbc' // JDBC 지원
// 데이터베이스 관련 의존성
implementation 'com.h2database:h2' // 예시로 H2 데이터베이스 사용 (필요에 맞게 DB 변경)
implementation 'mysql:mysql-connector-java:8.0.33' // MySQL 데이터베이스 연결
// OGNL 관련 의존성
implementation 'ognl:ognl:3.2.19'
// Thymeleaf 의존성 (중복된 의존성 제거)
implementation 'org.thymeleaf:thymeleaf:3.1.2.RELEASE' // 최신 버전 확인
}
tasks.named('test') {
useJUnitPlatform()
}
application.properties
server.port=5000
spring.application.name=GoogleEmail
# Datasource (HikariCP)
spring.datasource.hikari.maximum-pool-size=4
spring.datasource.username=root
spring.datasource.password=1234
spring.datasource.url=jdbc:mysql://localhost:3306/study?useSSL=false&serverTimezone=UTC
# JPA
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.show-sql=true
# Email
spring.mail.host=smtp.gmail.com
spring.mail.port=587
spring.mail.username=이메일
spring.mail.password=발급받은 비밀번호
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true
spring.mail.properties.mail.smtp.starttls.required=true
spring.mail.properties.mail.smtp.connectiontimeout=5000
spring.mail.properties.mail.smtp.timeout=5000
spring.mail.properties.mail.smtp.writetimeout=5000
spring.mail.auth-code-expiration-millis=1800000 # 30 * 60 * 1000 == 30?
# Redis
spring.data.redis.host=localhost
spring.data.redis.port=6380
EmailConfig.java
package com.study.GoogleEmail.api.common.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.JavaMailSenderImpl;
import java.util.Properties;
@Configuration
public class EmailConfig {
@Value("${spring.mail.host}")
private String host;
@Value("${spring.mail.port}")
private int port;
@Value("${spring.mail.username}")
private String username;
@Value("${spring.mail.password}")
private String password;
@Value("${spring.mail.properties.mail.smtp.auth}")
private boolean auth;
@Value("${spring.mail.properties.mail.smtp.starttls.enable}")
private boolean starttlsEnable;
@Value("${spring.mail.properties.mail.smtp.starttls.required}")
private boolean starttlsRequired;
@Value("${spring.mail.properties.mail.smtp.connectiontimeout}")
private int connectionTimeout;
@Value("${spring.mail.properties.mail.smtp.timeout}")
private int timeout;
@Value("${spring.mail.properties.mail.smtp.writetimeout}")
private int writeTimeout;
@Bean
public JavaMailSender javaMailSender() {
JavaMailSenderImpl mailSender = new JavaMailSenderImpl();
mailSender.setHost(host);
mailSender.setPort(port);
mailSender.setUsername(username);
mailSender.setPassword(password);
mailSender.setDefaultEncoding("UTF-8");
mailSender.setJavaMailProperties(getMailProperties());
return mailSender;
}
private Properties getMailProperties() {
Properties properties = new Properties();
properties.put("mail.smtp.auth", auth);
properties.put("mail.smtp.starttls.enable", starttlsEnable);
properties.put("mail.smtp.starttls.required", starttlsRequired);
properties.put("mail.smtp.connectiontimeout", connectionTimeout);
properties.put("mail.smtp.timeout", timeout);
properties.put("mail.smtp.writetimeout", writeTimeout);
return properties;
}
}
RedisConfig .java
package com.study.GoogleEmail.api.common.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.StringRedisTemplate;
@Configuration
public class RedisConfig {
@Bean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
return new StringRedisTemplate(redisConnectionFactory);
}
}
RedisUtil.java
package com.study.GoogleEmail.api.common.util;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
@Component
public class RedisUtil {
private final RedisTemplate<String, String> redisTemplate;
public RedisUtil(RedisTemplate<String, String> redisTemplate) {
this.redisTemplate = redisTemplate;
}
public void setDataExpire(String key, String value, long timeout) {
redisTemplate.opsForValue().set(key, value, timeout);
}
public String getData(String key) {
return redisTemplate.opsForValue().get(key);
}
public boolean existData(String key) {
return redisTemplate.hasKey(key);
}
public void deleteData(String key) {
redisTemplate.delete(key);
}
}
EmailController.java
package com.study.GoogleEmail.api.controller;
import com.study.GoogleEmail.api.dto.EmailDto;
import com.study.GoogleEmail.api.service.EmailService;
import jakarta.mail.MessagingException;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
@RequiredArgsConstructor
@RestController
@RequestMapping("/api/v1/email")
public class EmailController {
private final EmailService emailService;
// 인증코드 메일 발송
@CrossOrigin(origins = "http://localhost:3000") // React 앱의 주소
@PostMapping("/send")
public String mailSend(@RequestBody EmailDto emailDto) throws MessagingException {
System.out.println("EmailController.mailSend()"); // System.out.println 사용
emailService.sendEmail(emailDto.getMail());
return "인증코드가 발송되었습니다.";
}
// 인증코드 인증
@CrossOrigin(origins = "http://localhost:3000") // React 앱의 주소
@PostMapping("/verify")
public String verify(@RequestBody EmailDto emailDto) {
System.out.println("EmailController.verify()"); // 로그 출력
boolean isVerify = emailService.verifyEmailCode(emailDto.getMail(), emailDto.getVerifyCode());
// 인증 코드 비교 후 로그 추가
if (isVerify) {
System.out.println("인증 코드 일치: " + emailDto.getVerifyCode());
} else {
System.out.println("인증 코드 불일치: 입력된 코드 " + emailDto.getVerifyCode());
}
return isVerify ? "인증이 완료되었습니다." : "인증 실패하셨습니다.";
}
}
PageController.java
package com.study.GoogleEmail.api.controller;
import com.study.GoogleEmail.api.service.EmailService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.ui.Model;
import jakarta.mail.MessagingException;
@Controller
@RequiredArgsConstructor
public class PageController {
private final EmailService emailService;
// 이메일 인증 요청 페이지를 보여주는 GET 메서드
@GetMapping("/email/sendPage")
public String showSendEmailPage() {
return "emailSend"; // 이메일 전송을 위한 페이지 (emailSend.html)
}
// 이메일 인증 확인 페이지를 보여주는 GET 메서드
@GetMapping("/email/verifyPage")
public String showVerifyEmailPage(@RequestParam String email, Model model) {
model.addAttribute("email", email); // 이메일을 뷰로 전달
return "emailVerify"; // 인증 확인 페이지
}
// 이메일 인증 요청을 처리하는 POST 메서드
@GetMapping("/email/send")
public String sendEmail(@RequestParam String mail) throws MessagingException {
emailService.sendEmail(mail); // 이메일 전송
return "redirect:/email/verifyPage?email=" + mail; // 인증 페이지로 리다이렉트
}
// 이메일 인증 코드를 확인하는 POST 메서드
@GetMapping("/email/verify")
public String verifyEmail(@RequestParam String mail, @RequestParam String verifyCode, Model model) {
boolean isVerified = emailService.verifyEmailCode(mail, verifyCode);
if (isVerified) {
model.addAttribute("message", "인증 성공");
} else {
model.addAttribute("message", "인증 실패");
}
return "emailResult"; // 인증 결과 페이지 (emailResult.html)
}
}
EmailDto.java
package com.study.GoogleEmail.api.dto;
public class EmailDto {
private String mail;
private String verifyCode;
// Getter와 Setter
public String getMail() {
return mail;
}
public void setMail(String mail) {
this.mail = mail;
}
public String getVerifyCode() {
return verifyCode;
}
public void setVerifyCode(String verifyCode) {
this.verifyCode = verifyCode;
}
}
Email.java
package com.study.GoogleEmail.api.entity;
import jakarta.persistence.*;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Getter
@NoArgsConstructor
@Entity
@Table(name = "email")
public class Email {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "email_id", unique = true, nullable = false)
private Long id;
// 이메일 주소
@Column(name = "email", nullable = false)
private String email;
// 이메일 인증 여부
@Column(name = "email_status", nullable = false)
private boolean emailStatus;
@Builder
public Email(String email) {
this.email = email;
this.emailStatus = false;
}
}
EmailRepository.java
package com.study.GoogleEmail.api.repository;
import com.study.GoogleEmail.api.entity.Email;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface EmailRepository extends JpaRepository<Email, Long> {
// 인증코드 발송한 이메일 주소 조회
public Optional<Email> findByEmail(String email);
}
EmailService.java
package com.study.GoogleEmail.api.service;
import com.study.GoogleEmail.api.common.util.RedisUtil;
import jakarta.mail.MessagingException;
import jakarta.mail.internet.MimeMessage;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.stereotype.Service;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;
import org.thymeleaf.templatemode.TemplateMode;
import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver;
import java.util.Random;
@Slf4j
@Service
@RequiredArgsConstructor
public class EmailService {
private final JavaMailSender javaMailSender;
private final RedisUtil redisUtil;
private static final String senderEmail = 내 구글 이메일;
private String createCode() {
int leftLimit = 48; // number '0'
int rightLimit = 122; // alphabet 'z'
int targetStringLength = 6;
Random random = new Random();
return random.ints(leftLimit, rightLimit + 1)
.filter(i -> (i <= 57 || i >= 65) && (i <= 90 || i >= 97)) // 0-9, A-Z, a-z만 사용
.limit(targetStringLength)
.collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append)
.toString();
}
// 이메일 내용 초기화
private String setContext(String code) {
Context context = new Context();
TemplateEngine templateEngine = new TemplateEngine();
ClassLoaderTemplateResolver templateResolver = new ClassLoaderTemplateResolver();
context.setVariable("code", code);
templateResolver.setPrefix("templates/");
templateResolver.setSuffix(".html");
templateResolver.setTemplateMode(TemplateMode.HTML);
templateResolver.setCacheable(false);
templateEngine.setTemplateResolver(templateResolver);
return templateEngine.process("mail", context);
}
// 이메일 폼 생성
private MimeMessage createEmailForm(String email) throws MessagingException {
String authCode = createCode();
MimeMessage message = javaMailSender.createMimeMessage();
message.addRecipients(MimeMessage.RecipientType.TO, email);
message.setSubject("안녕하세요. 인증번호입니다.");
message.setFrom(senderEmail);
message.setText(setContext(authCode), "utf-8", "html");
// Redis 에 해당 인증코드 인증 시간 설정
redisUtil.setDataExpire(email, authCode, 60 * 30L); // 인증 코드 30분 동안 유효
return message;
}
// 인증코드 이메일 발송
public void sendEmail(String toEmail) throws MessagingException {
if (redisUtil.existData(toEmail)) {
redisUtil.deleteData(toEmail);
}
// 이메일 폼 생성
MimeMessage emailForm = createEmailForm(toEmail);
// 이메일 발송
javaMailSender.send(emailForm);
}
// 코드 검증
public Boolean verifyEmailCode(String email, String code) {
String codeFoundByEmail = redisUtil.getData(email);
log.info("이메일: {}로부터 요청된 인증 코드: {}", email, code);
log.info("저장된 인증 코드: {}", codeFoundByEmail);
if (codeFoundByEmail == null) {
log.warn("저장된 인증 코드가 존재하지 않습니다. 이메일={}", email);
return false; // 인증 코드가 존재하지 않으면 false 반환
}
// 공백 제거 후 비교
codeFoundByEmail = codeFoundByEmail.trim();
boolean isCodeValid = codeFoundByEmail.equals(code);
if (isCodeValid) {
log.info("인증 코드 일치: 입력된 코드 = {}, 저장된 코드 = {}", code, codeFoundByEmail);
} else {
log.warn("인증 코드 불일치: 이메일={} 입력된 코드={} 저장된 코드={}", email, code, codeFoundByEmail);
}
return isCodeValid;
}
}
GoogleEmailApplication.java
package com.study.GoogleEmail;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class GoogleEmailApplication {
public static void main(String[] args) {
SpringApplication.run(GoogleEmailApplication.class, args);
}
}
프론트앤드
App.tsx
import React, { useState } from "react";
import "./App.css";
const App: React.FC = () => {
const [email, setEmail] = useState<string>("");
const [isEmailSent, setIsEmailSent] = useState<boolean>(false);
const [userCode, setUserCode] = useState<string>(""); // 사용자가 입력한 인증 코드
const [isVerified, setIsVerified] = useState<boolean>(false);
// 이메일 입력 변화 처리
const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setEmail(e.target.value);
};
// 인증 코드 입력 변화 처리
const handleCodeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setUserCode(e.target.value);
};
// 인증 코드 전송 함수
const sendVerificationCode = async () => {
try {
// 로컬 서버에서 인증번호 전송 API 호출
const response = await fetch("http://localhost:5000/api/v1/email/send", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ mail: email }),
});
if (!response.ok) {
throw new Error("이메일 전송에 실패했습니다.");
}
const data = await response.text(); // 서버에서 받은 응답을 텍스트로 처리
setIsEmailSent(true); // 이메일이 성공적으로 전송됨
alert(data); // 서버로부터 받은 메시지 표시
} catch (error) {
console.error(error);
alert("이메일 전송에 실패했습니다.");
}
};
// 인증 코드 확인 함수
const verifyCode = async () => {
try {
// 로컬 서버에서 인증번호 확인 API 호출
const response = await fetch("http://localhost:5000/api/v1/email/verify", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
mail: email,
verifyCode: userCode,
}),
});
if (!response.ok) {
throw new Error("인증번호 확인에 실패했습니다.");
}
const data = await response.text(); // 서버로부터 응답받은 메시지
if (data === "인증이 완료되었습니다.") {
setIsVerified(true);
alert(data); // 인증 성공 메시지
} else {
alert("잘못된 인증번호입니다. 다시 시도해주세요.");
}
} catch (error) {
console.error(error);
alert("인증번호 확인에 실패했습니다.");
}
};
return (
<div className="App">
<h1>이메일 인증 테스트</h1>
<div className="email-input">
<input
type="email"
placeholder="이메일을 입력하세요"
value={email}
onChange={handleEmailChange}
/>
<button onClick={sendVerificationCode}>인증번호 전송</button>
</div>
{isEmailSent && (
<div className="code-input">
<input
type="text"
placeholder="인증번호를 입력하세요"
value={userCode}
onChange={handleCodeChange}
/>
<button onClick={verifyCode}>인증번호 확인</button>
</div>
)}
{isVerified && <p>인증이 완료되었습니다!</p>}
</div>
);
};
export default App;
App.css
.App {
text-align: center;
margin-top: 50px;
font-family: Arial, sans-serif;
}
.email-input,
.code-input {
margin-bottom: 20px;
}
input {
padding: 10px;
margin: 10px;
border: 1px solid #ccc;
border-radius: 5px;
}
button {
padding: 10px 20px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
button:hover {
background-color: #45a049;
}
테스트 시작
테스트 화면
메일 입력후 인증번호 전송
구글이메일에 인증번호 도착
인증번호 입력하면
인증이 완료되었습니다! 나오면 성공
'코드 모듈화 프로젝트' 카테고리의 다른 글
2. Google 로그인 구현(Oauth2) (0) | 2024.11.11 |
---|---|
코드 모듈화 프로젝트란? (3) | 2024.11.11 |