✅ 작동 순서
유저가 회원가입 하면서 이메일 입력
➡️ 서버에서 이메일로 인증번호 이메일 발송
➡️ 인증번호는 Redis에 저장(만료시간 5분)
➡️ 유저가 이메일에서 인증번호 확인
➡️ 이메일과 인증번호로 서버에 인증 요청
➡️ 서버에서 Redis있는 인증번호와 비교
➡️ 인증 완료 혹은 exceptions처리
💡 Redis
in-memory data structure store that can be used as a database, cache, and message broker
- caching, session management
- save frequently used data in memory, to reduce the laod off the primary data storage.
- 단 5분동안만 사용할 인증번호를 데이터베이스에 저장하고 삭제하는 것은 번거로운 일이며 비용도 많이 든다. 따라서 인메모리인 Redis에 저장하여 속도를 높이고 비용을 절약한다.
💡 @Valid 사용하여 유효성 검사
@Valid를 붙여 놓으면 check validation on input data.
예를 들어,
public ResponseEntity<String> createUser(@Valid @RequestBody User user)
이렇게 User
앞에 @Valid
를 붙여 놓으면 User
를 @RequestBody
로 가져오기 전에 유효성을 검사하고 가져온다.
✅ gmail 2단계 인증 설정
- 앱 비밀번호 받기
- 구글 계정 관리
- 보안
- google에 로그인 하는 방법 -> 2단계 인증 설정
- 구글 검색창에 “앱 비밀번호 검색”
- 앱 비밀번호 생성
- 이 비밀번호는 나중에 yaml파일에 jasypt로 암호화 해서 넣어둘 것이니 잘 기억해두기
build.gradle에 의존성 추가
1
2
3
4
5
6
7
8
9
| // email 인증
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-mail', version: '2.6.3'
implementation 'javax.mail:mail:1.4.7'
// Spring Context Support
implementation 'org.springframework:spring-context-support:5.3.9'
// redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
|
✅ 인증번호 전송하는 email
EmailCertificationConfig
- 이메일 보낼 email Address(yaml에서 관리)
- 아까 생성한 앱 비밀번호(yaml에서 관리)
- 이메일을 어떻게 보낼 것인가? `JavaMailSender
JavaMailSender
host, port, javaMailProperties 정하기
- 랜덤 인증번호 생성 메소드
generateRandomNumber
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
| 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;
import java.util.Random;
@Configuration
public class EmailCertificationConfig {
@Value("${email.address}")
private String emailAddress;
@Value("${email.app-password}")
private String appPassword;
@Bean
public JavaMailSender mailSender(){ //JAVA MAILSENDER 인터페이스를 구현한 객체를 빈으로 등록하기 위함.
JavaMailSenderImpl mailSender= new JavaMailSenderImpl(); // JavaMailSender 의 구현체 생성
mailSender.setHost("smtp.gmail.com"); // 이메일 전송에 사용할 SMTP 서버 호스트를 설정
mailSender.setPort(587); // 587로 포트를 지정
mailSender.setUsername(emailAddress); //구글계정을 넣습니다.
mailSender.setPassword(appPassword); //구글 앱 비밀번호를 넣습니다
Properties javaMailProperties= new Properties(); //JavaMail의 속성을 설정하기 위해 Properties 객체를 생성
javaMailProperties.put("mail.transport.protocol", "smtp"); //프로토콜로 smtp 사용
javaMailProperties.put("mail.smtp.auth", "true"); //smtp 서버에 인증이 필요
javaMailProperties.put("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory"); //SSL 소켓 팩토리 클래스 사용
javaMailProperties.put("mail.smtp.starttls.enable", "true"); //STARTTLS(TLS를 시작하는 명령)를 사용하여 암호화된 통신을 활성화
javaMailProperties.put("mail.debug", "true"); //디버깅 정보 출력
javaMailProperties.put("mail.smtp.ssl.trust", "smtp.naver.com"); //smtp 서버의 ssl 인증서를 신뢰
javaMailProperties.put("mail.smtp.ssl.protocols", "TLSv1.2"); //사용할 ssl 프로토콜 버젼
mailSender.setJavaMailProperties(javaMailProperties); //mailSender에 우리가 만든 properties 넣고
return mailSender;
}
// min과 max 사이의 랜덤한 정수를 반환하는 메서드
public static int generateRandomNumber(int min, int max){
Random random= new Random();
return random.nextInt(max - min + 1) + min;
}
}
|
EmailRequest DTO
유저의 이메일 받아오기
Validation을 사용해 유효성 검사
1
2
3
4
5
6
7
| @Getter
@Setter
public class EmailRequest {
@Email(message= "이메일 형식을 확인해주세요.")
@NotEmpty(message="이메일을 입력해주세요.")
private String email;
}
|
EmailCertificationController
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
| import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.shoppingMall.service.service.EmailCertificationService;
import org.shoppingMall.service.service.RedisUtil;
import org.shoppingMall.web.DTO.email.EmailRequest;
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;
@RestController
@RequiredArgsConstructor
@RequestMapping("/mail")
public class EmailCertificationController {
private final EmailCertificationService emailCertificationService;
private final RedisUtil redisUtil;
@PostMapping("/send-mail")
public String sendMail(@RequestBody @Valid EmailRequest emailRequest){
System.out.println("이메일 인증 이메일 : " + emailRequest.getEmail());
return emailCertificationService.joinEmail(emailRequest.getEmail());
}
}
|
EmailCertificationService
- 이메일 보내는 메소드(yaml에서 관리)
- 이메일 보낼 email Address
- 이메일 제목
- 이메일 내용
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
| import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import static org.shoppingMall.config.certification.EmailCertificationConfig.generateRandomNumber;
@Service
@RequiredArgsConstructor
public class EmailCertificationService {
private final JavaMailSender mailSender;
private final RedisUtil redisUtil;
private final int authNumber= generateRandomNumber(100000, 999999); // config 에 미리 만들어둔 메서드
@Value("${email.address}")
private String emailAddress; // email-config에 설정한 자신의 이메일 주소를 입력
public String joinEmail(String email) {
String setFrom= emailAddress;
String toMail= email;
String title= "[인증]OOO사이트 가입 인증번호";
String content=
"회원가입 창으로 돌아가 인증 번호를 정확히 입력해주세요." + //html 형식으로 작성 !
"<br><br>" +
"인증 번호는 " + authNumber + "입니다." +
"<br>" ; //이메일 내용 삽입
mailSend(setFrom, toMail, title, content);
return Integer.toString(authNumber);
}
@Transactional
public void mailSend(String setFrom, String toMail, String title, String content) {
MimeMessage message= mailSender.createMimeMessage(); //JavaMailSender 객체를 사용하여 MimeMessage 객체를 생성
try{
MimeMessageHelper helper= new MimeMessageHelper(message, true, "utf-8"); //이메일 메시지와 관련된 설정을 수행합니다.
// true를 전달하여 multipart 형식의 메시지를 지원하고, "utf-8"을 전달하여 문자 인코딩을 설정
helper.setFrom(setFrom); //이메일의 발신자 주소 설정
helper.setTo(toMail); //이메일의 수신자 주소 설정
helper.setSubject(title); //이메일의 제목을 설정
helper.setText(content, true);
mailSender.send(message); //이메일의 내용 설정 두 번째 매개 변수에 true를 설정하여 html 설정으로한다.
}catch(MessagingException e){ //이메일 서버에 연결할 수 없거나, 잘못된 이메일 주소를 사용하거나, 인증 오류가 발생하는 등 오류
// 이러한 경우 MessagingException이 발생
e.printStackTrace(); //e.printStackTrace()는 예외를 기본 오류 스트림에 출력하는 메서드
}
}
}
|
✅ Bean으로 Jasypt 암호화 설정
⭐️⭐️⭐️ 여기까지 하고 테스트하면 메일이 발송되어야 한다.
✅ Redis
Redis 설치
https://soheeparklee.github.io/posts/Redis-gitCommands/
EmailCheckRequest DTO 생성
유저가 인증번호 받은 후, 이 인증번호를 확인할 때 쓰는 DTO Validation을 사용해 유효성 검사
1
2
3
4
5
6
7
8
9
| @Data
public class EmailCheckRequest {
@Email
@NotEmpty(message = "이메일 형식을 확인해주세요.")
private String email;
@NotEmpty(message = "인증 번호를 입력해 주세요.")
private String authNum;
}
|
checkAuthNum Controller 메소드를 EmailCertificationController에 추가
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
| @RestController
@RequiredArgsConstructor
@RequestMapping("/mail")
public class EmailCertificationController {
private final EmailCertificationService emailCertificationService;
private final RedisUtil redisUtil;
@PostMapping("/send-mail")
public String sendMail(@RequestBody @Valid EmailRequest emailRequest) {
System.out.println("이메일 인증 이메일 : " + emailRequest.getEmail());
return emailCertificationService.joinEmail(emailRequest.getEmail());
}
// checkAuthNum Controller 추가
@PostMapping("/check-auth-num")
public String checkAuthNum(@RequestBody @Valid EmailCheckRequest emailCheckRequest) {
Boolean checked = emailCertificationService.checkAuthNum(emailCheckRequest.getEmail(), emailCheckRequest.getAuthNum());
if (checked) {
return "OK";
}else {
// exception은 custom 하여 사용중
throw new BadRequestException("잘못된 인증 번호 입니다.", emailCheckRequest.getAuthNum());
}
}
}
|
RedisUtil
지정된 key에 해당하는 데이터를 Redis에서 가져오고, 저장하고
지정된 시간 후에는 데이터 만료, 그리고 삭제까지
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
29
30
31
32
33
34
| import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;
import java.time.Duration;
@Service
@RequiredArgsConstructor
public class RedisUtil {
private final StringRedisTemplate redisTemplate; // Redis에 접근하기 위한 Spring의 Redis 탬플릿 클래스
public String getData(String key){ // 지정된 key에 해당하는 데이터를 Redis에서 가져오는 메서드
ValueOperations<String, String> valueOperations= redisTemplate.opsForValue();
return valueOperations.get(key);
}
public void setData(String key, String value){ // 지정된 key에 값을 저장하는 메서드
ValueOperations<String, String> valueOperations= redisTemplate.opsForValue();
valueOperations.set(key, value);
}
public void setDataExpire(String key, String value, long duration){ // 지정된 key에 값을 지정하고, 지정된 시간 후에 데이터가 만료되도록 설정하는 메서드
ValueOperations<String, String> valueOperations= redisTemplate.opsForValue();
Duration expireDuration= Duration.ofSeconds(duration);
valueOperations.set(key, value, expireDuration);
}
public void deleteData(String key){ // 지정된 key 에 해당하는 데이터를 Redis에서 삭제하는 메서드
redisTemplate.delete(key);
}
}
|
✅ Exceptions
인증번호 틀리면 BadReqeustException 발생
ExceptionControllerAdvice
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| import lombok.extern.slf4j.Slf4j;
import org.shoppingMall.service.exceptions.*;
import org.shoppingMall.web.DTO.email.ErrorResponse;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
@Slf4j
public class ExceptionControllerAdvice {
@ResponseStatus(HttpStatus.BAD_REQUEST) //BadRequestException 가져올 떄 직접 만든 것으로 가져오기 주의, java원래 클래스 가져오면 에러
@ExceptionHandler(BadRequestException.class)
public ResponseEntity<ErrorResponse> handleBadRequestException(BadRequestException bre){
log.error("Bad Request Exception: "+ bre.getMessage());
ErrorResponse errorResponse= new ErrorResponse(400, "Bad Request Exception", bre.getDetailMessage(), bre.getRequest());
return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
}
}
|
BadReqeustException extends RunTimeException
1
2
3
4
5
6
7
8
9
10
| @Getter
public class BadRequestException extends RuntimeException{
private String detailMessage;
private Object request;
public BadRequestException(String detailMessage, Object request) {
this.detailMessage = detailMessage;
this.request = request;
}
}
|
ErrorResponse DTO
1
2
3
4
5
6
7
8
9
| @Getter
@AllArgsConstructor
public class ErrorResponse {
private Integer code;
private String message;
private String detailMessage;
private Object request;
}
|
✅ 마지막으로 EmailCertificationService에 인증 로직 추가
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
| import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import static org.shoppingMall.config.certification.EmailCertificationConfig.generateRandomNumber;
@Service
@RequiredArgsConstructor
public class EmailCertificationService {
private final JavaMailSender mailSender;
private final RedisUtil redisUtil;
private final int authNumber= generateRandomNumber(100000, 999999); // config 에 미리 만들어둔 메서드
@Value("${email.address}")
private String emailAddress; // email-config에 설정한 자신의 이메일 주소를 입력
public String joinEmail(String email) {
String setFrom= emailAddress;
String toMail= email;
String title= "[인증]OOO사이트 가입 인증번호";
String content=
"회원가입 창으로 돌아가 인증 번호를 정확히 입력해주세요." + //html 형식으로 작성 !
"<br><br>" +
"인증 번호는 " + authNumber + "입니다." +
"<br>" ; //이메일 내용 삽입
mailSend(setFrom, toMail, title, content);
return Integer.toString(authNumber);
}
@Transactional
public void mailSend(String setFrom, String toMail, String title, String content) {
MimeMessage message= mailSender.createMimeMessage(); //JavaMailSender 객체를 사용하여 MimeMessage 객체를 생성
try{
MimeMessageHelper helper= new MimeMessageHelper(message, true, "utf-8"); //이메일 메시지와 관련된 설정을 수행합니다.
// true를 전달하여 multipart 형식의 메시지를 지원하고, "utf-8"을 전달하여 문자 인코딩을 설정
helper.setFrom(setFrom); //이메일의 발신자 주소 설정
helper.setTo(toMail); //이메일의 수신자 주소 설정
helper.setSubject(title); //이메일의 제목을 설정
helper.setText(content, true);
mailSender.send(message); //이메일의 내용 설정 두 번째 매개 변수에 true를 설정하여 html 설정으로한다.
//redis에 인증번호 저장 로직 추가
redisUtil.setDataExpire(Integer.toString(authNumber), toMail, 60*5L); // ⭐️ 여기 추가, redis에 데이터 저장 // 유효기간 5분
}catch(MessagingException e){ //이메일 서버에 연결할 수 없거나, 잘못된 이메일 주소를 사용하거나, 인증 오류가 발생하는 등 오류
// 이러한 경우 MessagingException이 발생
e.printStackTrace(); //e.printStackTrace()는 예외를 기본 오류 스트림에 출력하는 메서드
}
}
public Boolean checkAuthNum(String email, String authNum) {
if(redisUtil.getData(authNum) == null) return false;
else if (redisUtil.getData(authNum).equals(email)) return true;
else return false;
}
}
|
✅ Run redis server on EC2
Redis server install
서버 시작하는 명령어
1
| sudo systemctl start redis
|
서버 상태 확인하는 명령어
1
| sudo systemctl status redis
|
redis.conf
- Redis.conf에 들어가서
1
| sudo nano /etc/redis/redis.conf
|
- 비밀번호 바꾸기(참고자료)
- protected-mode 바꾸기
- 방화벽 풀어주기
서버 재시작 명령어
1
| sudo systemctl restart redis
|
포트 6379 열기
redis는 보통 포트 6379에서 실행된다.
1
| sudo netstat -tuln | grep 6379
|
EC2와 연결된 redis서버 연결
1
| redis-cli -h -p 6379 -a
|
💡 참고
https://wookgu.tistory.com/26
Redis Config??? 필요가 없었다……
💡 참고
1
2
3
4
5
| https://velog.io/@jinny-l/spring-jasypt-encrypt-yml-and-store-encryption-key-as-environment-variable
-- 한솔이 블로그
https://boulder-hippodraco-244.notion.site/Email-Spring-Boot-3-2-52fa5aa2f1154691bd7c2a8fea3d89a6
|