앞서 네이버에서 제공하는 SMTP서버를 통해 사용자에게 인증코드를 전송시켰다.

서비스 로직에서 사용자가 입력한 인증번호를 검증하려면, 서버는 메일로 발송한 인증번호를 저장하고 있어야 한다.

인증번호는 짧은 시간 동안만 유효하며 이후에는 필요없기 때문에, 이런 조건에 맞춰 효율적으로 관리할 수 있는 Redis와 같은 인메모리 데이터 저장소를 사용하는 것이 적합하다고 판단했다. Redis는 데이터 액세스 속도와 *TTL(Time To Live) 설정 기능을 제공해 단기 데이터를 처리하는데 많이 사용한다고 한다.(Redis의 특징이 결국 Redis를 사용하는 이유)

 


1) Redis 설치

- Redis 서버 실행을 위해, .msi 파일 다운 및 설치

- 기본 포트 번호 6379

- 현재 배포된 3.0.504 버전은 설치 완료와 동시에 서비스에 자동등록

https://github.com/microsoftarchive/redis/releases

 

Releases · microsoftarchive/redis

Redis is an in-memory database that persists on disk. The data model is key-value, but many different kind of values are supported: Strings, Lists, Sets, Sorted Sets, Hashes - microsoftarchive/redis

github.com

 

 

2)  Redis 의존성 추가

<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-redis -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <version>3.4.0</version>
</dependency>

 

 

3) 연결 정보 설정

- application.properties 파일에 Redis 연결 정보 설정

(API 키, 비밀번호, 포트번호 등은 application.properties 파일에서 관리하도록 수정함)

#redis
spring.data.redis.host=localhost
spring.data.redis.port=6379

 

 

 

4) RedisConfig 설정

- @Value를 사용해 applications.properties에서 정의한 설정 값을 Spring Bean에 주입

package com.tofit.mvc.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.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig {
	@Value("${spring.data.redis.host}")
	private String host;
	
	@Value("${spring.data.redis.port}")
	private int port;
	
	@Bean
	public RedisConnectionFactory redisConnectionFactory() {
		return new LettuceConnectionFactory(host, port);
	}
	
	@Bean
	public RedisTemplate<String, Object> redisTemplate(){
		RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
		
		redisTemplate.setKeySerializer(new StringRedisSerializer());
		redisTemplate.setValueSerializer(new StringRedisSerializer());
		redisTemplate.setConnectionFactory(redisConnectionFactory());
		
		return redisTemplate;
	}
}

 

 

 

5) RedisService 구현

- setCode  : 주어진 이메일을 키로, 인증 코드를 값으로 Redis에 저장, 만료시간 3분 설정

- getCode : 주어진 이메일에 해당하는 인증코드를 Redis에서 조회

package com.tofit.mvc.model.service;

import java.util.concurrent.TimeUnit;

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;

@Service
public class RedisService {

	private final RedisTemplate<String, Object> redisTemplate;

	public RedisService(RedisTemplate<String, Object> redisTemplate) {
		super();
		this.redisTemplate = redisTemplate;
	}
	
	public void setCode(String email, String code) {
		ValueOperations<String, Object> valOperations = redisTemplate.opsForValue();
		
		// 만료시간 3분
		valOperations.set(email, code, 180, TimeUnit.SECONDS);
	}
	
	public String getCode(String email) {
		ValueOperations<String, Object> valOperations = redisTemplate.opsForValue();
		Object code = valOperations.get(email);
		
		if(code == null) {
			return null;
		}
		
		return code.toString();
	}
}

 

 

 

6) UserRestController에 이메일 인증 요청 완성

@PostMapping("/mail")
    public ResponseEntity<Boolean> mailConfirm(@RequestParam String email) throws Exception{
    	String code = UUID.randomUUID().toString().substring(0,6);
		boolean result = mailService.sendMail(code, email);
		
		// 메일 보내기 성공
		if(result) {
			// redis 저장
			redisService.setCode(email, code);
			return new ResponseEntity<Boolean>(true, HttpStatus.OK);
		}
		return new ResponseEntity<Boolean>(false, HttpStatus.BAD_REQUEST);
    }
	
	@PostMapping("/mail/confirm")
	public ResponseEntity<?> codeConfirm(@RequestBody EmailInfo emailInfo){
		String answerCode = redisService.getCode(emailInfo.getEmail());
		
		System.out.println(answerCode);
		
		if(answerCode == null)
			return new ResponseEntity<String>("코드 만료", HttpStatus.UNAUTHORIZED);
		
		if(answerCode.equals(emailInfo.getCode()))
			return new ResponseEntity<Boolean>(true, HttpStatus.OK);
		
		return new ResponseEntity<Boolean>(false, HttpStatus.NON_AUTHORITATIVE_INFORMATION);
	}

 


 

구현을 마치며

 

단순한 작업처럼 보였지만, 보안, 성능, 유지보수 등 여러 측면을 고려해야 한다는 점을 다시 한번 느꼈다. 진행하면서 발생한 에러가 여러가지가 있었는데, Maven 빌드 과정에서 application.properties 파일을 처리하면서 발생한 문제였고, 원인 파악에 많은 시간이 걸렸다. 파일을 자세히 보니 한글이 깨져있는 것을 반견했다. 깃헙에 프로젝트를 합치고 다시 개인브랜치로 가져와 작업하면서 파일의 인코딩 설정이 어느순간 변경된 것 같다. 다시 UTF-8로 바꿔서 문제를 해결할 수 있었다. 처음에 pom.xml에 의존성 추가하면서 problems창에 에러가 떴는데 이 원인일줄은 몰랐다. .m2 폴더 삭제하고 플러그 다시 설치하고 난리.. 

 

1. 목표

Tofit Project를 디벨롭하면서 추가하기로 한 기능 중 하나다. 서비스를 이용하기 위해서 회원가입이 필요하고, 회원가입 절차에서 최소한의 본인확인을 위한 인증 방식이 필요하다고 생각했다. 뿐만 아니라, 비밀번호 재설정에서도 임시 비밀번호 발급에 해당 방식을 많이 사용하고 있기 때문에 공부해보기로 했다.

 

**이메일 인증에는 2가지 방식이 있다.**

1) 메일로 받은 인증번호를 직접 입력해서 인증하기.

2) 인증링크를 받아 클릭해서 인증하기. 

 

사용자 편의성을 생각하면 두번째 방식이 좋지만, 보안이 중요한 서비스들은 대부분 인증번호를 직접 인증하는 방식을 사용하기 때문에 2가지 방식 중 첫 번째 방식을 채택해서 구현해보기로했다.

 


2. 네이버 메일 SMTP와 Redis를 사용한 이메일 인증 기능을 구현

1) 이메일 프로토콜?

컴퓨터와 서버 간의 통신을 규제하는 규칙으로, 서로 다른 시스템에서 이메일을 주고받을 수 있도록 해줌.

 

메시지 전송 [SMTP] : 사용자의 메일 클라이언트에서 메일 서버로 메시지를 전송하는 역할

메시지 수신 [POP3/IMAP] : 메일 서버에서 사용자의 메일 클라이언트로 메시지를 수신하는 역할

 

POP3와 IMAP 중에서 메일을 한 디바이스에서만 사용하고, 서버에서 메시지를 저장하지 않아도 되기 때문에 POP3 사용.

 

2) Redis?

메모리 기반의 Key-Value 구조의 비관계형 DB. 간편하게 Map 형태로 데이터 저장이 가능하고, 데이터 저장 및 조회에 빠른 속도를 보장해줌.

 

자체적으로 데이터 만료 기능 제공함으로, 설정한 시간이 지나면 저절로 데이터가 소멸하기 때문에 유효성 확인을 위한 별도의 로직을 구현할 필요가 없어진다. 때문에 사용자 이메일에 대한 인증 코드가 있는지 조회만 하면 됨으로 이메일 인증을 쉽게 구현하기 위해 사용하기로 했다

 


3. 네이버 메일 설정

메일 > 환경설정 > POP3/SMTP 설정

 

 


4. Spring Boot + SMTP

1) 의존성 추가

- Spring Boot Starter Mail 조회 후 가장 최신 버전으로 추가했다.

 <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-mail -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-mail</artifactId>
    <version>3.4.0</version>
</dependency>

 

 

 

2) MailConfig

- Spring에서 메일 서버 사용하기 위해서는 메일 서버와 연결해야한다. 구글은 조금 더 과정이 필요해서 네이버를 통해 연결해줬다.

- 이메일과 비밀번호는 SMTP 서버에 인증할 계정을 입력하면 된다.(applications.properties에서 읽도록 개선)

package com.tofit.mvc.config;

import java.util.Properties;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.JavaMailSenderImpl;

@Configuration
public class MailConfig {
	
	@Bean
	public JavaMailSender javaMailService() {
		JavaMailSenderImpl javaMailSender = new JavaMailSenderImpl();
		
		javaMailSender.setHost("smtp.naver.com");
		javaMailSender.setUsername("이메일 입력");
		javaMailSender.setPassword("비밀번호 입력");
	
		javaMailSender.setPort(465);
		
		javaMailSender.setJavaMailProperties(getMailProperties());
		
		return javaMailSender;
	}

	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;
	}
}

 

 

3) MailService

- 이메일 인증 코드 발송 서비스

- JavaMailSender를 통해 SMTP 서버를 사용해 이메일을 작성하고 전송한다.

package com.tofit.mvc.model.service;

import java.util.UUID;

import org.springframework.mail.MailException;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.stereotype.Service;

import io.micrometer.core.instrument.config.validate.Validated.Secret;
import jakarta.mail.Message;
import jakarta.mail.MessagingException;
import jakarta.mail.internet.InternetAddress;
import jakarta.mail.internet.MimeMessage;

@Service
public class MailService {
	
	private final JavaMailSender javaMailSender;
	
	public MailService(JavaMailSender javaMailSender) {
		this.javaMailSender = javaMailSender;
	}

	private MimeMessage createMessage(String code, String email) throws MessagingException {
		MimeMessage message = javaMailSender.createMimeMessage();
		
		message.addRecipients(Message.RecipientType.TO, email); 
		message.setSubject("ToFit 회원가입 인증 번호입니다.");
		message.setText("이메일 인증코드: " + code); 
		message.setFrom("수신자 이메일 주소 입력");
		return message;
		
	}
	
	public boolean sendMail(String code, String email) throws Exception {
		try {
			MimeMessage mimeMessage = createMessage(code, email);
			javaMailSender.send(mimeMessage);
			return true;
		} catch (MailException mailException) {
			mailException.printStackTrace();
			throw new IllegalAccessException();
		}
	}
}

 

 

4) UserRestController에 추가

@PostMapping("/mail")
    public ResponseEntity<Boolean> mailConfirm(@RequestParam String email) throws Exception{
    	String code = UUID.randomUUID().toString().substring(0,6);
		boolean result = mailService.sendMail(code, email);
		
		// 메일 보내기 성공
		if(result) {
			return new ResponseEntity<Boolean>(true, HttpStatus.OK);
		}
		return new ResponseEntity<Boolean>(false, HttpStatus.BAD_REQUEST);
    }

 

 

 

 

 

'SPRING' 카테고리의 다른 글

이메일인증 (2) - Redis로 인증코드 관리하기  (4) 2024.12.27

+ Recent posts