본문 바로가기
API

[스프링] 회원가입, 비밀번호 찾기 이메일 인증 구현하기

by 지지 2022. 3. 29.

개발환경

자바11, 스프링 프레임워크, 메이븐, Mybatis, MySQL, 인텔리제이


현재 개인 프로젝트를 진행 중에 있는데, 구현해본적이 없는 기능들 위주로 진행해보기로 했다.

그 중 하나가 이메일 전송 기능!

 

회원가입 시 이메일 인증을 해보려고 한다.(비밀번호 찾기도 동일한 방식으로 진행할 수 있다.)

이메일 인증에는 두 가지 경우가 있다.

 

1. 인증번호를 받아 인증번호 입력으로 이메일 인증하기.

2. 인증링크를 받아 링크 클릭 시 인증 완료하기.

 

두 가지 경우 중 나는 후자인 링크로 인증하는 방법을 구현했다.

또한, 구글을 이용한 이메일 인증을 했다.

 

(참고로, 기본적인 DTO(또는 VO)나 서비스, 컨트롤러 등은 다 준비되어 있다는 가정하에 진행합니다.

회원 테이블에는 본인이 설계한 아이디, 비밀번호, 이름 등의 컬럼 외에 mail_key, mail_auth 컬럼이 추가로 필요합니다!

mail_key는 TempKey.java에서 생성한 난수를 저장하는 데에 사용되며,

mail_auth는 기본값 0을 넣어 놓고, 이메일 인증을 했을 경우 값을 1로 변경시켜 로그인이 가능하게 합니다.

 

컬럼명은 마음대로 지정하시면 됩니다. 혹시 해당 컬럼이 없으신 분들은 아래 명령어로 추가해주세요.)

alter table 테이블명 add mail_auth int default 0;

alter table 테이블명 add mail_key varchar(50);

회원가입 시 이메일 인증 구현하는 방법

1. 아래 주소에 들어가서 보안 수준이 낮은 앱 허용을 사용안함에서 사용으로 바꿔준다.

보안 수준이 낮은 앱의 액세스 (google.com)

 

[2022/11/16 수정]

위 '보안 수준이 낮은 앱의 액세스' 설정은 2022년 5월 30일 이후 서비스가 종료되었다고 합니다.

대신 2단계 인증 활성화를 통해 구글 이메일 보내기를 구현할 수 있습니다!

 

1. 아래 주소에 들어가서 2단계 인증 활성화를 해준다.

-> 여기서 설정해준 구글 계정이 이메일을 보내는 발신 계정이 된다.

-> 참고로 나는 구글 계정을 하나 더 만들어서 내가 개발하고 있는 사이트에서 메일을 보내기 위한 용도로만 사용하고 있다.

-> 아래 주소를 클릭해도 되고, Google 계정 관리 -> 보안 탭을 클릭해 직접 이동해도 된다.

2단계 인증 활성화(google.com)

 

Google 계정

보안 계정을 안전하게 보호하기 위해 보안 설정을 검토 및 조정하고 권장사항을 받아보려면 계정에 로그인하세요.

myaccount.google.com

 

1) 위 주소로 들어가면 아래와 같은 화면이 나오는데, 2단계 인증을 클릭해 전화 또는 문자로 인증을 진행한다.

2) 2단계 인증을 활성화 시키면 앱 비밀번호라는 부분이 추가되어 있다.

3) 앱 비밀번호를 클릭하면 앱 비밀번호를 생성할 앱 및 기기를 선택하라고 나오는데, 메일-windows 컴퓨터를 선택했다.

4) 앱 비밀번호를 생성하면 아래와 같이 16자리의 비밀번호가 생성된다!

-> 이 비밀번호가 gmail 비밀번호 대신 사용하게 될 비밀번호이므로 어디에 잘 적어놓자.

 

2. pom.xml에 메일 라이브러리 추가

pom.xml

<!-- mail library -->
<dependency>
    <groupId>javax.mail</groupId>
    <artifactId>mail</artifactId>
    <version>1.4.7</version>
</dependency>

<!-- mail 서포트 -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context-support</artifactId>
    <version>${org.springframework-version}</version>
</dependency>

-> 다들 알겠지만 maven은 pom.xml에 라이브러리를 추가하면 업데이트를 꼭 해주어야 한다!

-> 이클립스 : 프로젝트 우클릭 - maven - update project

-> 인텔리제이 : 우측 상단 update 아이콘 클릭 or 프로젝트 우클릭 - Maven - Reload project

 

3. email-context.xml 추가 (또는 MailConfig.java 추가)

-> src - main - webapp - WEB-INF - spring 경로에 만들어주면 된다.

-> email라이브러리의 빈을 등록해주는 과정이다.

-> 여기에 아까 발급받은 앱 비밀번호를 넣어준다.(google계정 비밀번호 아님!)

 

email-context.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!-- email 인증 관련   -->
    <bean id="mailSender" class="org.springframework.mail.javamail.JavaMailSenderImpl">
        <property name="host" value="smtp.gmail.com" />
        <property name="port" value="587" />
        <property name="username" value="gmail계정"/>
        <property name="password" value="앱 비밀번호" />
        <property name="javaMailProperties">
            <props>
                <prop key="mail.transport.protocol">smtp</prop>
                <prop key="mail.smtp.auth">true</prop>
                <prop key="mail.smtp.starttls.enable">true</prop>
                <prop key="mail.debug">true</prop>
                <prop key="mail.smtp.ssl.trust">smtp.gmail.com</prop>
                <prop key="mail.smtp.ssl.protocols">TLSv1.2</prop>
            </props>
        </property>
    </bean>
</beans>

 

[참고] xml파일 대신 java파일로 설정해도 된다. -> MailConfig.java 추가 (xml, java 둘 중 하나만 해주기!)

@Configuration
public class MailConfig {

    @Bean
    public JavaMailSender javaMailSender() {

        Properties mailProperties = new Properties();
        mailProperties.put("mail.transport.protocol", "smtp");
        mailProperties.put("mail.smtp.auth", "true");
        mailProperties.put("mail.smtp.starttls.enable", "true");
        mailProperties.put("mail.smtp.debug", "true");
        mailProperties.put("mail.smtp.ssl.trust", "smtp.gmail.com");
        mailProperties.put("mail.smtp.ssl.protocols", "TLSv1.2");

        JavaMailSenderImpl mailSender = new JavaMailSenderImpl();
        mailSender.setJavaMailProperties(mailProperties);
        mailSender.setHost("smtp.gmail.com");
        mailSender.setPort(587);
        mailSender.setUsername("gmail아이디");
        mailSender.setPassword("앱 비밀번호");
        mailSender.setDefaultEncoding("utf-8");
        return mailSender;
    }
}

 

4. email-context.xml을 읽을 수 있도록 web.xml에 코드 추가

-> /WEB-INF/spring/email-context.xml 만 한 줄 추가해주면 된다.

<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>
        /WEB-INF/spring/root-context.xml
        <!-- 추가 시작 -->
        /WEB-INF/spring/email-context.xml
        <!-- 추가 끝 -->
    </param-value>
</context-param>

 

5. MailHandler.java, TempKey.java 추가

-> 파일 위치는 src - main - java - 본인 패키지 아무 데나 넣어도 된다. 나는 mail패키지를 따로 만들어 넣어주었다.

빨간색 박스는 무시하자..ㅎ

MailHandler.java

import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;

import javax.activation.DataSource;
import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import java.io.UnsupportedEncodingException;

public class MailHandler {
    private JavaMailSender mailSender;
    private MimeMessage message;
    private MimeMessageHelper messageHelper;

    public MailHandler(JavaMailSender mailSender) throws MessagingException {
        this.mailSender = mailSender;
        message = this.mailSender.createMimeMessage();
        messageHelper = new MimeMessageHelper(message, true, "UTF-8");
    }

    public void setSubject(String subject) throws MessagingException {
        messageHelper.setSubject(subject);
    }

    public void setText(String htmlContent) throws MessagingException {
        messageHelper.setText(htmlContent, true);
    }

    public void setFrom(String email, String name) throws UnsupportedEncodingException, MessagingException {
        messageHelper.setFrom(email, name);
    }

    public void setTo(String email) throws MessagingException {
        messageHelper.setTo(email);
    }

    public void addInline(String contentId, DataSource dataSource) throws MessagingException {
        messageHelper.addInline(contentId, dataSource);
    }

    public void send() {
        mailSender.send(message);
    }
}

-> 메일 전송 라이브러리의 setter이다.

-> 위부터 메일 제목, 내용, 발송자, 수신자, 보내기(send)로 이루어져 있다.

 

TempKey.java

import java.util.Random;

public class TempKey{
    private boolean lowerCheck;
    private int size;

    public String getKey(int size, boolean lowerCheck) {
        this.size = size;
        this.lowerCheck = lowerCheck;
        return init();
    }

    private String init() {
        Random ran = new Random();
        StringBuffer sb = new StringBuffer();
        int num  = 0;
        do {
            num = ran.nextInt(75) + 48;
            if ((num >= 48 && num <= 57) || (num >= 65 && num <= 90) || (num >= 97 && num <= 122)) {
                sb.append((char) num);
            } else {
                continue;
            }
        } while (sb.length() < size);
        if (lowerCheck) {
            return sb.toString().toLowerCase();
        }
        return sb.toString();
    }
}

-> 인증번호를 보낼 때 사용할 클래스이다. 이 클래스를 호출할 때는 몇 자리 수로 할 건지 사이즈를 파라미터로 보내면 된다.

-> 여러 블로그들을 보니 실제 이메일을 보내는 코드에 위 코드를 넣는 경우도 많았다. 하지만 나는 각 클래스나 메서드를 각자의 역할에 맞게 코드를 분리했고, 추후 비밀번호 찾기를 할 때도 위 클래스를 사용할 것이기 때문에 따로 분리를 해줬다!

 

여기까지 기본 설정과 꼭 필요한 파일들을 생성해줬다. 이제부턴 로직을 짜고 이메일을 보내보자!

 

(다시 한번 말씀드리지만, 여기까지 따라 했다면 기본적인 DTO(또는 VO)나 서비스, 컨트롤러 등은 다 준비되어 있다는 가정하에 아래 로직을 진행합니다.

회원 테이블에는 본인이 설계한 아이디, 비밀번호, 이름 등의 컬럼 외에 mail_key, mail_auth 컬럼이 추가로 필요합니다!

mail_key는 TempKey.java에서 생성한 난수를 저장하는 데에 사용되며,

mail_auth는 기본값 0을 넣어 놓고, 이메일 인증을 했을 경우 값을 1로 변경시켜 로그인이 가능하게 합니다.

 

컬럼명은 마음대로 지정하시면 됩니다. 혹시 해당 컬럼이 없으신 분들은 아래 명령어로 추가해주세요.)

alter table 테이블명 add mail_auth int default 0;

alter table 테이블명 add mail_key varchar(50);

 

6. memberMapper.xml 에 쿼리 추가

-> 총 세 개의 쿼리를 추가해 준다.

 

1) 회원가입 시 이메일 인증을 위한 랜덤번호 저장

<update id="updateMailKey" parameterType="MemberDto">
    update member set mail_key=#{mail_key} where email=#{email} and id=#{id}
</update>

-> 회원가입 시 회원 테이블의 mail_key컬럼에 랜덤으로 생성한 키를 넣어주는 쿼리이다.

-> where 절은 본인이 주고 싶은 조건을 주면 된다. 나는 email중복을 허용하기 때문에 id까지 확인하는 쿼리로 짜줬다.

 

2) 메일 인증을 하면 mail_auth 컬럼을 기본값 0에서 1로 바꿔 로그인을 허용

<update id="updateMailAuth" parameterType="MemberDto">
    update member set mail_auth=1 where email=#{email} and mail_key=#{mail_key}
</update>

-> 메일 인증을 하면 mail_auth값을 0에서 1로 바꿔주는 쿼리이다.

-> 잠시 후 메일을 보낼 때, 인증 링크에 회원의 이메일과 mail_key를 쿼리 스트링에 같이 보낼건데, 사실 쿼리스트링에 회원 이메일만 보내고, where절에는 email만 넣어주어도 충분히 인증이 가능하다. 그렇다면 mail_key도 함께 확인하는 이유가 무엇일까? 너무 궁금했는데, 적어도 내가 읽어본 블로그 글들은 이유를 설명해 주지 않았다.. 내가 생각하는 이유와, 지인에게 들은 답변 두 개는 아래와 같다.

-> 첫 번째, 만일 mail_key를 확인하지 않는다면, url에 직접 컨트롤러의 경로와 회원의 email만 넣어주면 이메일 링크를 통하지 않더라도 메일 인증을 할 수 있게된다. 해커의 공격은 둘째치고, 당연히 이렇게 되는건 백엔드 개발자로써 용납할 수 없는 로직이다. 그렇기 때문에 랜덤으로 생성된 키값을 같이 보내준다.

-> 두 번째, mail_key를 보냄으로써 메일이 내 서버에서 보낸 인증메일이 맞는지 확인하는 용도이다. 음.. 사실 위의 이유와 같은 맥락의 이유라고 볼 수 있다. 내 db에 저장된 키값과 메일인증 링크를 눌렀을 때 보내주는 키값을 확인하여 일치하는 경우에만 mail_auth를 update 해주려는 이유이다.

 

3) 이메일 인증을 안 했으면 0을 반환, 로그인 시 인증했나 안 했나 체크하기 위함

<select id="emailAuthFail" parameterType="String" resultType="int">
    select count(*) from member where id=#{id} and mail_auth=1
</select>

-> 이 쿼리는 회원가입 - 메일 인증 후 로그인을 할 때 필요한 쿼리이다.

-> 로그인 시 mail_auth를 확인해서 0이면 이메일 인증을 하지 않은 것, 1이면 이메일 인증을 완료한 것이다.

-> 만일 여기서 0이 반환됐다면, '이메일 인증 후 다시 로그인을 시도해달라'라는 경고창을 띄어주면 된다.

 

7. MemberDao, MemberDaoImpl, MemberService에 코드 추가

 

MemberDao.java

int updateMailKey(MemberDto memberDto) throws Exception;
int updateMailAuth(MemberDto memberDto) throws Exception;
int emailAuthFail(String id) throws Exception;

 

MemberDaoImpl.java

@Override
public int updateMailKey(MemberDto memberDto) throws Exception {
    return session.update(namespace + "updateMailKey", memberDto);
}

@Override
public int updateMailAuth(MemberDto memberDto) throws Exception {
    return session.update(namespace + "updateMailAuth", memberDto);
}

@Override
public int emailAuthFail(String id) throws Exception {
    return session.selectOne(namespace + "emailAuthFail", id);
}

 

MemberService.java

int updateMailKey(MemberDto memberDto) throws Exception;
int updateMailAuth(MemberDto memberDto) throws Exception;
int emailAuthFail(String id) throws Exception;

 

7. MemberServiceImpl에 코드 추가

 

1) MemcerService.java 파일에 적어준 코드를 오버라이드 해주는 코드를 작성

@Override
public int updateMailKey(MemberDto memberDto) throws Exception {
    return memberDao.updateMailKey(memberDto);
}

@Override
public int updateMailAuth(MemberDto memberDto) throws Exception {
    return memberDao.updateMailAuth(memberDto);
}

@Override
public int emailAuthFail(String id) throws Exception {
    return memberDao.emailAuthFail(id);
}

 

2) 회원 등록(insertMember) 메서드에 인증메일 보내는 코드 추가!

@Service
public class MemberServiceImpl implements MemberService{

    @Autowired
    MemberDao memberDao;
    @Autowired
    JavaMailSender mailSender;
    
    @Override
    public void insertMember(MemberDto memberDto) throws Exception {
        //랜덤 문자열을 생성해서 mail_key 컬럼에 넣어주기
        String mail_key = new TempKey().getKey(30,false); //랜덤키 길이 설정
        memberDto.setMail_key(mail_key);

        //회원가입
        memberDao.insertMember(memberDto);
        memberDao.updateMailKey(memberDto);

        //회원가입 완료하면 인증을 위한 이메일 발송
        MailHandler sendMail = new MailHandler(mailSender);
        sendMail.setSubject("[RunninGo 인증메일 입니다.]"); //메일제목
        sendMail.setText(
                "<h1>RunninGo 메일인증</h1>" +
                "<br>RunninGo에 오신것을 환영합니다!" +
                "<br>아래 [이메일 인증 확인]을 눌러주세요." +
                "<br><a href='http://localhost:8080/join/registerEmail?email=" + memberDto.getEmail() +
                "&mail_key=" + mail_key +
                "' target='_blank'>이메일 인증 확인</a>");
        sendMail.setFrom("보내는사람@이메일", "러닝고");
        sendMail.setTo(memberDto.getEmail());
        sendMail.send();
    }

-> 의존성 추가 잊지 말고 해주자.

->sendMail.setFrom("보내는사람@이메일", "러닝고") : 보내는사람 이메일에는 본인이 위 1번에서 설정해주었던 이메일 주소를 넣어주면 된다.

 

여기까지 완료했다면, 회원가입을 해보자. 입력한 이메일로 메일이 잘 전송되는 것을 확인할 수 있다.

다음은 위 쿼리 스트링에 보내준 경로를 받아주는 컨트롤러를 만들어주자.

 

7. JoinController에 코드 추가

@GetMapping("/registerEmail")
public String emailConfirm(MemberDto memberDto)throws Exception{

    memberService.updateMailAuth(memberDto);

    return "/member/emailAuthSuccess";
}

-> 인증메일을 보낼 때 <a> 태그에 보내준 경로와 매핑되는 컨트롤러이다. '이메일 인증 확인' 링크를 누르면 이 컨트롤러로 이동하게 된다.

-> emailConfirm 메서드를 만들어 준 후 아까 쿼리를 짜준 updateMailAuth를 넣어주면, email과 mail_key값이 일치한다면 mail_auth컬럼이 0에서 1로 바뀌게 되고, 이메일 인증 완료 페이지로 넘어가게 된다.

-> 이메일 인증 완료 페이지는 각자 작성해주면 될 것 같다.ㅎㅎ... 참고로 나는 아래 이미지와 같이 그냥 스크립트 alert으로 간단하게만 만들어주었다.

emailAuthSuccess.jsp

 

자, 여기까지 했다면, 성공적으로 이메일도 보내졌고, 인증 링크를 누르면 인증에 성공했다는 alert까지 나오게 된다.

그러나 여기까지만 한다면, 이메일 인증을 했던 안 했던 로그인이 가능하게 된다. 왜? mail_key가 1일 때만 로그인을 가능하게 하는 코드를 짜주지 않았으니까!

이제 거의 다 왔다. LoginController에 코드만 추가해주면 이메일 인증은 끝이다!

 

8. LoginController에 코드 추가

@PostMapping("/login")
    public String login(String id, MemberDto memberDto, Model model) throws Exception {

        //로그인 시 아이디, 비밀번호 일치여부 확인
        if(memberService.login(memberDto) != 1){
            model.addAttribute("loginFailMsg", "아이디 또는 비밀번호가 올바르지 않습니다.");
            return "/member/loginForm";
        }

        //이메일 인증 했는지 확인
        if (memberService.emailAuthFail(id) != 1) {
            return "/member/emailAuthFail";
        }

        return "redirect:/";
    }

-> 로그인을 할 때 이메일 인증을 하지 않은 상태라면 emailAuthFail 페이지로 넘어가게 했다. 이 페이지는 이메일 인증을 성공했을 때와 마찬가지로 스크립트로 간단하게 처리해주었다.

emailAuthFail.jsp

 

 

여기까지 회원가입 시 이메일 인증을 구현해보았다. 이 로직을 토대로 비밀번호 찾기 시 이메일을 전송하는 코드도 쉽게 짤 수 있을 것이다. 간단한 듯 복잡한 듯 알고 보면 간단한 이메일 인증 끝!


[참고]

이메일 인증을 잘 구현했다면, 한 가지 아쉬운 점이 생길 것이다.

그것은 바로 이메일 전송 속도! 하나의 메일을 보내는데 3~4초 정도가 소요되는데, 이 문제를 해결하려면 '쓰레드'나 '비동기'의 개념을 알아야한다. 이 문제를 해결했던 개인적인 경험을 블로그에 작성했다. 참고해보길 바란다!

https://jee2memory.tistory.com/entry/이메일_전송이_왜_이렇게_느려

 

[개발일지] 이메일 전송이 왜 이렇게 느려? (feat. @Async)

이메일 전송이 왜 이렇게 느려? 취준시절 RunningGo라는 작은 사이드 프로젝트를 혼자 진행했었다. 해당 프로젝트에서 회원가입, 비밀번호 찾기 시에 이메일을 전송하는 기능을 구현했는데, (당시

jee2memory.tistory.com

 

 

모든 소스는 github에 올라가 있습니다!

https://github.com/jeejee1106/ToyProject-RunningGo

 

GitHub - jeejee1106/ToyProject-RunningGo: 서울의 러닝 장소를 추천하는 커뮤니티

서울의 러닝 장소를 추천하는 커뮤니티. Contribute to jeejee1106/ToyProject-RunningGo development by creating an account on GitHub.

github.com

'API' 카테고리의 다른 글

[스프링부트] 아임포트 카카오(정기)결제 API 사용하기  (1) 2022.01.24

댓글