본문 바로가기

프로젝트/✍️ [전공자들]

[전공자들 9] 회원가입 기능구현 (이메일 인증, 중복체크)

 

회원가입 프론트 화면

 

회원가입 구현 시 백엔드 처리 순서

 

1. 이메일 중복확인

2. 이메일 인증

3. 닉네임 중복확인

4. 데이터베이스에 저장

 


1. 이메일 중복확인 + 닉네임 중복확인

- 프론트에서 요청 보내기

axiosURL.post('/member/email/exists', {email:email})
      .then(res => {
        if (res.data) {
          setOnAuthEmail(true)
          setPassEmail(true)
          sendEmail()
        } else {
          setPassEmail(false)
          setOnAuthEmail(false)
        }
      }).catch(error => {
        console.log(error) 
      })

 

중복확인은 단순 조회지만 이메일은 개인정보이기 때문에 그냥 post처리. 

RequestBody에 이메일 실어서 백엔드에 요청

 

- 백엔드 처리

 @PostMapping("/email/exists") //이메일인증
    public ResponseEntity<Boolean> existsEmail(@RequestBody Map<String,String> body) {
        try {
            String email = body.get("email");
            Optional<MemberDto> member = memberService.getMemberByEmail(email);
            if (member.isEmpty()) { //중복확인 통과
                return new ResponseEntity<Boolean>(true, HttpStatus.OK);
            } else {
                return new ResponseEntity<Boolean>(false, HttpStatus.OK);
            }
        } catch (Exception e) { 
            return new ResponseEntity<Boolean>(HttpStatus.BAD_REQUEST);
        }
    }

 

body에서 이메일을 찾아 memberService에 넘겨줌. 

받은 이메일로 memberService는 memberRepository를 통해 데이터베이스에서 멤버를 찾아옴

public interface MemberRepository extends MongoRepository<MemberDto, String> {
    public Optional<MemberDto> findByEmail(String email) throws Exception;
    public Optional<MemberDto> findByNickname(String nickname) throws Exception;
}

 

만약 멤버가 존재하지 않으면 중복확인 통과 -> true반환

멤버가 존재하면 중복확인 실패 -> false 반환

 

 

- 내려온 응답에 따른 프론트 처리

false반환일경우
true반환일경우 인증번호 전송 및 입력칸 띄우기


이메일 인증

 

준비과정

프론트로부터 이메일 받는 과정은 생략하겠음.

먼저, 메일을 보낼 때 사용할 이메일 정보를 설정해야한다. 

구글, 네이버 등등 사용할 사이트의 메일 설정에 들어가서 기본 설정을 바꿔줘야 하는데 

이 과정은 구글에 스프링부트 메일보내기  등등으로 검색하면 쉽게 따라할 수 있다.

 

나는 구글 메일을 사용했는데, 내 실제 이메일, 비밀번호를 하드코딩해 github에 올릴 수 없으니 환경변수로 등록하여 

프로젝트 코드 내에서 다루지 않도록 했다. 

 

시스템 환경변수 설정

작업표시줄 시스템 환경 변수 검색 > 시스템 변수  > 새로만들기 > 값 설정

 

그리고 application.properties에 이메일을 보내기 위한 사용자 정보를 추가했다. 

( db등 각종 연결정보를 담는 application.properties 도 .gitignore에 등록하여 github에 올리지 않도록 하는 것이 좋다.)

spring.mail.host=smtp.gmail.com
spring.mail.port=587
spring.mail.username=${MAJORS_EMAIL_USERNAME}
spring.mail.password=${MAJORS_EMAIL_PASSWORD} 

 

다음으로 spring에서 제공하는 메일 보내기 기능을 위해서 javaMailSender 의존성을 주입한다.

implementation 'org.springframework.boot:spring-boot-starter-mail'

 

그리고 javaMailSender를 @Bean으로 등록해준다. 

package com.binunu.majors.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 org.springframework.validation.annotation.Validated;

import java.util.Properties;

@Configuration
public class MailConfig {
    @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;
    @Bean
    public JavaMailSender javaMailSender(){
        JavaMailSenderImpl mailSender = new JavaMailSenderImpl();
        mailSender.setHost(host);
        mailSender.setPort(port);
        mailSender.setUsername(username);
        mailSender.setPassword(password);

        Properties props = mailSender.getJavaMailProperties();
        props.put("mail.transport.protocol","smtp");
        props.put("mail.smtp.auth","true");
        //TSL 설정
        props.put("mail.smtp.starttls.enable","true");
        props.put("mail.smtp.starttls.required","true");

        return mailSender;
    }
}

 

사용할 메일 서버와 포트, 내 이메일과 비밀번호를 가져와서 설정.

 

여기까지 했으면 준비는 끝났다. 

 

-Controller

package com.binunu.majors.membership.controller;

import com.binunu.majors.membership.service.EmailService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.Map;

@RestController
@RequestMapping("member")
public class EmailController {
    private final EmailService emailService;
    @Autowired
    public EmailController(EmailService emailService){
        this.emailService = emailService;
    }
    @PostMapping("/email/send")
    public ResponseEntity<String> sendAuthNumByEmail(@RequestBody Map<String,String> body){
        String email = body.get("email");

        try{
            String code=emailService.sendAuthNumByEmail(email);
            return new ResponseEntity<String>(code, HttpStatus.OK);
        }catch (Exception e){
            e.printStackTrace();
            return new ResponseEntity<String>(HttpStatus.BAD_REQUEST);

        }
    }
}

 

컨트롤러는 그냥 서비스에 이메일을 전달해주는 역할만 함

 

-Service

package com.binunu.majors.membership.service;


import jakarta.mail.internet.MimeMessage;
import org.springframework.beans.factory.annotation.Autowired;
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 java.util.Random;

@Service
public class EmailServiceImpl implements EmailService {
    private final JavaMailSender javaMailSender;
    public EmailServiceImpl(JavaMailSender javaMailSender){
        this.javaMailSender = javaMailSender;
    }
    
    @Value("${spring.mail.host}")
    private String host;

    //인증번호 생성로직
    @Override
    public String generateVerificationCode() throws Exception {
        int length = 6;
        String characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
        Random random = new Random();
        StringBuilder sb = new StringBuilder();

        for (int i = 0; i < length; i++){
            int index = random.nextInt(characters.length());
            sb.append(characters.charAt(index));
        }

        return sb.toString();
    }

    @Override
        public String sendAuthNumByEmail(String email) throws Exception {
            String code = generateVerificationCode();
            MimeMessage message = javaMailSender.createMimeMessage();
            MimeMessageHelper helper = new MimeMessageHelper(message,true,"UTF-8");

            helper.setTo(email);
            helper.setFrom(host,"전공자들");
            helper.setSubject("[전공자들] 회원가입 인증 번호 안내");
            String htmlText = "<h2>전공자들 인증 번호 안내</h2>"+
                    "<br/>"+
                    "<p>회원님께서 요청하신 전공자들 회원 가입을 위한 이메일 인증 번호를 알려드립니다.</p>"+
                    "<br/>"+
                    "<p>아래 인증코드를 복사하여 입력해주시기 바랍니다.</p>"+
                    "<br/>"+
                    "<h3>인증코드 : "+code+"</h3>"+
                    "<br/>";
            helper.setText(htmlText, true);
            javaMailSender.send(message);
            return code;
        }
}

 

service는 두 개의 메소드로 구성되어 있음. 

generateVerificationCode() 는 랜덤한 값의 인증 코드를 생성해 주는 함수이고, 

sendAuthNumByEmail(String email) 이 메일을 보내는 함수이다. 

 

sendAuthNumByEmail에서 메일을 보내기 위해 MimeMessage객체를 생성한다. 

코드의 핵심은 MimeMessage 객체의 to, from, subject, text 에 각각 받는사람, 보내는사람, 메일제목, 메일내용을 담아서 전송하는게 다인데 부가적인 코드들이 많아서 복잡해 보일 수도 있다. 이 함수를 자세히 설명해 보면 다음과 같다.

 

@Override
    public String sendAuthNumByEmail(String email) throws Exception {
        String code = generateVerificationCode(); //인증 코드 생성
        MimeMessage message = javaMailSender.createMimeMessage(); //전송할 메일 생성
        
        //MimeMessage를 더 쉽게 사용할 수 있도록 해주는 클래스
        MimeMessageHelper helper = new MimeMessageHelper(message,true,"UTF-8");
        

        helper.setTo(email); //파라미터로 받아온 받는사람 이메일. 
        helper.setFrom(host,"전공자들"); //보내는 사람 이메일. *1)
        helper.setSubject("[전공자들] 회원가입 인증 번호 안내"); //메일 제목
        String htmlText = "<h2>전공자들 인증 번호 안내</h2>"+ // 메일 내용
                "<br/>"+
                "<p>회원님께서 요청하신 전공자들 회원 가입을 위한 이메일 인증 번호를 알려드립니다.</p>"+
                "<br/>"+
                "<p>아래 인증코드를 복사하여 입력해주시기 바랍니다.</p>"+
                "<br/>"+
                "<h3>인증코드 : "+code+"</h3>"+
                "<br/>";
        helper.setText(htmlText, true); //*2)
        javaMailSender.send(message); //메일전송
        return code; //프론트에 입력받은 인증번호와 대조하기 위해 code를 리턴해줌.
    }

 

*1)

 helper.setFrom(host,"전공자들")

 

javaMailSender 를 bean등록하는 과정에서 보내는 사람의 정보를 입력했는데 왜 여기서 또 설정하냐면 

사실, setFrom을 따로 설정하지 않아도 정상적으로 이메일을 보낼 수 있다. 

하지만 실제 메일을 받으면

 

이런식으로 개인이 보낸 메일처럼 직접적인 이메일 주소로 발송된다.

MimeMesageHelper의 setFrom에 두개의 인자로 첫번째 인자는 발신메일, 두번째 인자로 발신인명을 지정할 수 있다.

 

 

*2) 

helper.setText(htmlText, true)

 

메세지 객체는 MimeMessage 말고도 SimpleMailMessage이 있다. 

하지만 SimpleMailMessage는 단순한 텍스트만를 전송시킬 수 있는 반면, 

MimeMessage는 메일을 단락별로 나눠서 html형식으로 나름 꾸며서 보낼 수 있는 장점이 있다. 

 

 

하지만 만약 HTML형식의 이메일을 받을 수 없는 클라이언트에서는 작성한 html태그들이 모두 하드코딩되어 보여지기 때문에 MimeMessageHelper에서는 setText()의 두 번째 인자로 true를 설정하면 HTML을 텍스트로 fallback하는 기능을 제공한다.  

 

개발일정 + 우선순위 상 유효기간 설정을 못했는데, 다른 기능을 마무리하며 디테일을 추가할 예정이다... 

 

 

4. 데이터베이스에 저장

 const submitSet = (e) => {
    e.preventDefault()
    console.log(passEmail, passAuthNum, passPassword, passNickname)
    if (!passEmail || !passAuthNum || !passPassword || !passNickname) {
      e.preventDefault()
      alert("입력한 정보를 다시 확인해주세요")
    }

    const requestData = {
      name : name,
      email: email, 
      password: password,
      nickname: nickname,
      major: major,
      graduate: graduate,  
    };
 
    axiosURL.post('/member/join',requestData)
    .then(res => { 
      setState('Last')
    }).catch(err => {
      console.log(err)
    }) 
  }

 

원래는 formData를 사용해 프론트에서 받아오는 로직을 작성했었는데 , 자꾸 에러가 났다. 

알고보니 formData는 파일을 업로드 할 때 사용하기 위한 객체라는 것이었다. 

 

그래서 직접 객체를 만들어 post형식으로 body에 담아 전송했다. 

memberRepository의 save()를 통해 db에 저장했고,

 

원래 백엔드에서 비밀번호를 암호화하는 과정을 거쳐 데이터베이스에 저장해야하지만

아직 spring-security 의존성을 주입하지 않아서 내일 진행할 예정이다...

 

일단 데이터베이스에는 잘 들어간다.