회원가입시 이메일인증 구현하기

김재현·2023년 12월 21일
0

TIL

목록 보기
64/88

회원가입시 이메일인증 구현하기

이전에 '게시판 만들기' 프로젝트를 진행하며 이미 회원가입 기능을 구현한 바 있다. 그것에 더해 이번에 요구사항이 추가되었다.

바로 이메일을 통해 인증번호를 전달 받아 인증 절차는 거치는 것!

어떻게 이메일을 보낼 수 있는 것인지 모르기 때문에, 먼저 Spring에서 메일 보내는 법 부터 알아봤다.


JavaMail

알아보니 SpringBoot의 JavaMailSender 사용하여 이메일을 전송 할 수 있었다. 이걸 위해서는 몇가지 설정이 필요하다.

JavaMail 설정

1. 의존성 추가

dependencies {
    // Spring Boot Starter for Mail
    implementation 'org.springframework.boot:spring-boot-starter-mail'
}

2-1. SMTP 설정 추가

spring.mail.host=smtp.gmail.com
spring.mail.port=587
spring.mail.username=${Email_Adress}
spring.mail.password=${APP_Password}
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true

gmail을 이용하여 메일을 보내는 설정을 했다.
gmail을 털리긴 싫기 때문에 아이디 및 비밀번호는 환경변수로 설정했다.

이 때 주의할 점은 비밀번호는 gmail 비밀번호가 아니라, App Password라는 것이다. (처음엔 google 비밀번호인줄 알고 헤맸다.)

2-2. 구글 앱 비밀번호

앱 비밀번호란 보안 수준이 낮은 앱 또는 기기에 Google 계정에 대한 액세스 권한을 부여하는 16자리 비밀번호이다.
(앱 비밀번호는 2단계 인증이 사용 설정된 계정에서만 이용할 수 있다.)

2차 인증을 활성화하면 'google 계정'의 '보안' 탭에서 '앱 비밀번호' 항목을 검색 할 수 있다.

여기서 앱 비밀번호를 생성하고 잘 저장해둔다.
(최초 생성시 한번만 보여주기 때문에 잃어버리면 확인 할 수 없다.)

앱 비밀번호 생성

이렇게 생성된 비밀번호를 SMTP 설정의 spring.mail.password에 입력해주면 메일 보내기 위한 설정 완료이다.

JavaMail 사용

JavaMailSender 를 사용하기 위한 설정을 완료했으니 이제 사용을 해본다.

@Service
@RequiredArgsConstructor
public class EmailService {

    private final JavaMailSender javaMailSender;

    public void sendVerificationCodeByEmail(String to, String verificationCode) {
        SimpleMailMessage message = new SimpleMailMessage();
        message.setTo(to);
        message.setSubject("회원 가입 인증 번호");
        message.setText("인증번호: " + verificationCode);

        javaMailSender.send(message);
    }
}

이렇게 SimpleMailMessage로 발송할 메일 객체를 생성 한 뒤,
Bean으로 받아온 JavaMailSender 로 발송을 보낼 수 있다.

메서드

  • 사용된 메서드들에 대한 설명은 다음과 같다.
    • To : 발송할 메일 주소 입력
    • Subject : 발송되는 메일의 제목
    • Text : 발송되는 메일의 내용
    • .send() : 만들어진 SimpleMailMessage 의 객체를 발송

이제 메일 보내는법은 알았으니 인증 과정을 어떻게 거쳐야할까?


메일 인증 과정

먼저 이메일 인증을 위한 emailAuth라는 패키지를 생성했다.

1. EmailAuth

EmailAuth 엔터티를 만들어서 회원가입시 입력받는 nickname, password(encoded), email 정보들과 발송된 인증번호를 저장하도록 했다. (repository도 함께 만들었다.)

1-1. EmailAuth entity

@Entity
@Getter
@NoArgsConstructor
public class EmailAuth {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String sentCode;

    private String nickname;

    private String password;

    private String email;
			
            
            /** Constructor **/
}

1-2. EmailAuthService

그 뒤, Serive에서 메일 전송 기능, 인증번호 생성 기능, 인증번호를 인증하는 세가지 기능을 만들었다.

@Service
@RequiredArgsConstructor
public class EmailAuthService {

    private final EmailService emailService;
    private final EmailAuthRepository emailAuthRepository;


    public String sendVerificationCode(String email) {
        String sentCode = generateRandomCode();

        // 이메일로 인증 번호 발송
        emailService.sendVerificationCodeByEmail(email, sentCode);
        return sentCode;
    }

    public void verifyVerificationCode(String email, String verificationCode) {
        // 저장된 인증 번호와 사용자가 입력한 인증 번호를 비교
        var emailAuth = emailAuthRepository.findByEmail(email).orElse(null);
        if (emailAuth!=null && !emailAuth.getSentCode().equals(verificationCode)) {
            throw new IllegalArgumentException("인증번호가 일치하지 않습니다.");
        }
    }

    private String generateRandomCode() {
        // 랜덤한 6자리 숫자 생성
        Random random = new Random();
        int code = 100000 + random.nextInt(900000);
        return String.valueOf(code);
    }
}

2. UserService

이제 회원가입 과정에 넣을 인증 기능의 준비가 끝났으니 회원 가입 로직을 변경해보자!

2-1. 회원가입 정보 입력 및 인증 메일 발송

기존에는 입력된 정보를 확인 후 user 객체가 생성되어 User테이블에 저장되었지만, 이메일 인증을 추가해야하기 때문데 이 부분을 없앴다. 변경된 코드는 메일을 발송하고 Service 로직이 끝난다.

UserService의 signup 메서드

			... (입력된 정보 확인)

// 기존 코드       
// userRepository.save(new User(nickname,password,email));


// 변경된 코드: 인증번호 메일 보내기
String sentCode = emailAuthService.sendVerificationCode(email);

emailAuthRepository.save(
		new EmailAuth(sentCode, newNickname, passwordEncoder.encode(newPassword),
        email));

인증 메일 발송 결과

여기까지 만들고 회원가입을 위해 요청을 보냈을 때, 아래와 같이 인증 메일이 받아졌다! 성공!

2-2. 입력된 인증 번호 처리

사용자는 받은 인증번호로 다시 요청을 보내 회원가입을 완료해야한다.

UserController

컨트롤러에서 다음과 같이 받아오도록 만들었다.

이 때 인증 번호 검증을 위해 email을 한번 더 입력하도록 만들었다.
(이미 한번 입력 했는데 또? -> 나로써는 불편한 점이다.)

    @GetMapping("/signup/email/{email}/code/{verificationCode}")
    public ResponseEntity<CommonResponseDto> verificateCode(@PathVariable String email,
                                                            @PathVariable String verificationCode) {
        String nickname = userService.verificateCode(email, verificationCode);
        return ResponseEntity.ok().body(new CommonResponseDto(nickname+"님 회원가입 완료.",HttpStatus.OK.value()));
    }

UserService의 verificateCode메서드

  • 아까 EmailAuthService에서 만들어뒀던 verifyVerificationCode를 사용하여 인증번호를 확인한다.

  • 그리고 emailAuth 저장해뒀던 정보들로 모든 정보를 다시 입력 받을 필요 없이 회원가입을 완료한다.

  • 인증 이후에는 유저 정보가 User 테이블에 들어가기 때문에, 중복되어 저장될 필요가 없으니 emailAuth는 삭제한다.

    // 이메일 인증 및 User 생성
    public String verificateCode(String email, String verificateCode) {
        var emailAuth = emailAuthRepository.findByEmail(email).orElseThrow(()
                -> new IllegalArgumentException("인증 가능한 이메일 주소가 아닙니다."));

        String nickname = emailAuth.getNickname();
        String password = emailAuth.getPassword();

        emailAuthService.verifyVerificationCode(email,verificateCode);

        userRepository.save(new User(nickname,password,email));

        //인증 완료되면 삭제
        emailAuthRepository.delete(emailAuth);
        return nickname;
    }

회원가입 결과 성공!

3. Redis를 활용한 5분 내 인증!

요구사항에 '5분 이내에 검증'이 포함되어있기 때문에 만료시간을 설정 할 수 있는 Redis를 활용했다.

3-1 Redis 의존성 추가

redis를 사용하기 위해 의존성을 추가한다.

	// redis
	implementation 'org.springframework.boot:spring-boot-starter-data-redis'

3-2 RedisConfig 설정

redis는 기본적으로 key-value 형식을 갖는다.
아래 설정으로 RedisTemplate이 문자열 key와 객체 value를 사용하는 Redis 작업을 수행할 수 있게 만든다.

  • RedisConnectionFactory 빈을 생성해서 Redis 서버와 연결한다.

  • RedisTemplate 빈 생성

    • setConnectionFactory(redisConnectionFactory): 앞서 생성한 RedisConnectionFactory를 설정하여 RedisTemplate을 Redis 서버에 연결.
    • setKeySerializer(new StringRedisSerializer()): Redis의 key를 문자열로 다루기 위해 key의 직렬화를 지정.
@Configuration
public class RedisConfig {

	// RedisConnectionFactory 빈 생성
    @Bean
    public RedisConnectionFactory redisConnectionFactory(){
        return new LettuceConnectionFactory();
    }

	// RedisTemplate 빈 생성
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        return redisTemplate;
    }
}

3-3 코드에 RedisTemplate 추가

UserService

  • RedisTemplate을 원하는 형태로 추가한다.

  • opsForValue() : Redis의 String 값을 다루기 위한 다양한 메서드

  • key, value, 만료시간, unit 입력하여 저장.

    private final RedisTemplate<String , String > redisTemplate;
    
    					...
                        
    public void signup(SignupRequestDto requestDto) {
         				
                        ...
                        
    redisTemplate.opsForValue().set(email, 
    								sentCode, 
                                    5*60*1000, 
                                    TimeUnit.MILLISECONDS);
	}

위에서 설정을 마쳐놨기 때문에 verificateCode에서 다음과 같이 원하던 기능(5분이 지나 만료가 되었는지 확인)을 사용 할 수 있게 된다.

    public String verificateCode(String email, String verificateCode) {
    
				...
                
        // 5분이 지났는지 검증
        if (redisTemplate.hasKey(email)==null) {
            redisTemplate.delete(email);
            throw new IllegalArgumentException("5분 초과, 다시 인증하세요");
        }

				...

        //인증 완료되면 삭제
        emailAuthRepository.delete(emailAuth);
        redisTemplate.delete(email);
        return nickname;
    }

처음엔 메일 발송을 어떻게 해야하는지 막막했는데, 검색하고 시도해보면서 결국엔 성공했다. redis까지 사용해다니! 뿌듯하지 아니할 수 없다.

코드에서 개선 할 부분이 꽤 보이는데, 내일은 코드를 다듬고 버그를 찾아봐야겠다.


관련 포스팅

Following Post

profile
I live in Seoul, Korea, Handsome

0개의 댓글