Notice
Recent Posts
Recent Comments
Link
«   2024/11   »
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
Tags
more
Archives
Today
Total
관리 메뉴

개발자입니다

[프로젝트] 네이버 메일 링크 클릭으로 인증 구현(SpringBoot) 본문

네이버클라우드 AIaaS 개발자 양성과정 1기/프로젝트

[프로젝트] 네이버 메일 링크 클릭으로 인증 구현(SpringBoot)

끈기JK 2023. 4. 8. 19:43

 

개요

 

SpringBoot 프로젝트에서 메일 인증을 네이버 SMTP 이용해서 구현한다.

따로 인증 코드를 입력하지 않고 인증 링크를 클릭하면 메일 인증 되는 방식으로 진행하였다.

 

흐름은 다음과 같다.

- 회원 가입시 입력한 메일 주소로 인증 링크를 보낸다.

- 메일 사용자가 그 링크를 클릭하면 메일 인증을 완료한다.

- 로그인이 가능해진다. 인증하지 않으면 alert 창을 띄워 메일 인증 후 로그인 하라고 한다.

- 랜덤으로 36자리 문자를 만드는 UUID 사용할 것이기 때문에 편법으로 인증 시도는 매우 힘들 것이라 생각한다.

 

 

 

메일 계정 준비

 

네이버 메일 서비스를 이용하였다.

인증 메일 발송을 위해 계정을 새로 생성한다.

[환경설정 > POP3/IMAP 설정 > POP3/SMTP 설정] 들어간다.

적용 범위: 기존에 받은 메일을 포함하여 받음 선택

원본 저장 : 네이버 메일에 원본 저장 선택

후 저장한다.

 

 

 

의존 라이브러리 준비

 

현재 개발환경은 아래와 같다.

JavaSE: 17

SpringBoot : v3.0.5 (jakarta EE)

 

build.gradle 에 의존 라이브러리 준비한다.

    implementation 'org.springframework.boot:spring-boot-starter-mail:3.0.5'
    implementation 'org.springframework:spring-context:6.0.7'
    implementation 'org.springframework:spring-context-support:6.0.7'
    implementation 'com.sun.mail:jakarta.mail:2.0.1'

 

간혹 $ gradle eclipse 만으로도 의존성 문제로 에러가 발생하는 경우가 있다.

에러는 이런 문구로 시작한다.

DEBUG: Jakarta Mail version 1.0.0
2023-04-08T21:12:43.663+09:00 ERROR 29936 --- [  XNIO-1 task-5] io.undertow.request                      : UT005023: Exception handling request to /auth/signup

jakarta.servlet.ServletException: Handler dispatch failed: java.util.ServiceConfigurationError: jakarta.mail.Provider: com.sun.mail.imap.IMAPProvider not a subtype

 

chatGPT 문의 결과 아래 답변 받았으며, 이렇게 하니 해결되었다.

1. 프로젝트를 깨끗한 상태로 만들기 위해 Gradle 캐시를 정리합니다. 프로젝트 디렉토리에서 다음 명령어를 실행합니다.
./gradlew clean​

2. 프로젝트의 모든 Gradle 빌드 파일과 캐시를 삭제합니다. 이 작업을 수행하면 Gradle이 모든 의존성을 다시 다운로드하게 됩니다.
rm -rf ~/.gradle/caches​

3. 이클립스에서 프로젝트를 다시 가져옵니다. 이렇게 하면 프로젝트 설정이 최신 상태로 업데이트됩니다.

4. 이클립스에서 프로젝트를 "Clean"한 다음 "Build"를 실행합니다.

 

 

 

코드 작성

 

properties 파일 생성

mailAuth.properties 파일을 생성하고 src/main/resources 에 두었다.

이 파일을 생성하는 이유는 git 에 올릴때 민감한 정보는 .gitignore 에 등록해서 올리지 않도록 하기 위해서이다.

mail.username=네이버아이디
mail.password=네이버비밀번호

 

 

config 파일 생성

@PropertySource 로 위에서 작성한 프로퍼티 파일을 읽어들인다.

@ConfigurationProperties(prefix = "mail") 로 mail.username 의 값을 username 으로 받는다.

@Getter, @Setter 가 있어야 username, password 필드에 값을 설정할 수 있다.

네이버 메일 환경설정의 Host, Port 를 입력한다.

package bitcamp.app;
import java.util.Properties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.JavaMailSenderImpl;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

@Configuration
@PropertySource("classpath:mailAuth.properties")
@ConfigurationProperties(prefix = "mail")
@Getter
@Setter
@ToString
public class MailConfig {

  private String username;
  private String password;
  
  
  @Bean
  public JavaMailSender javaMailService() {
      JavaMailSenderImpl javaMailSender = new JavaMailSenderImpl();
      
      javaMailSender.setHost("smtp.naver.com"); // 메인 도메인 서버 주소 => 정확히는 smtp 서버 주소
      javaMailSender.setUsername(username); // 네이버 아이디
      javaMailSender.setPassword(password); // 네이버 비밀번호
      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"); // smtp 인증
      properties.setProperty("mail.smtp.starttls.enable", "true"); // smtp strattles 사용
      properties.setProperty("mail.debug", "true"); // 디버그 사용
      properties.setProperty("mail.smtp.ssl.trust","smtp.naver.com"); // ssl 인증 서버는 smtp.naver.com
      properties.setProperty("mail.smtp.ssl.enable","true"); // ssl 사용
      return properties;
  }
}

 

 

이 후 부터는 회원 가입 메일 인증 링크 클릭 을 따로 보면 편하다.

먼저 회원 가입 부분부터 쭉 본 후 메일 인증 링크 클릭을 쭉 보면 작업 흐름에 따라가는 것이다.

 

 

Controller 코드 수정

회원 가입

회원 가입을 하면 "/auth/signup" 으로 요청을 보낸다.

여기서 UUID 를 생성하고 token 으로 member 객체에 저장한다.

service 객체의 add() 를 이용한다.

 

메일 인증 링크 클릭

메일 인증 링크 클릭하면 "auth/verify" 로 요청을 보낸다.

링크에 미리 넣어놓은 UUID가 쿼리 스트링으로 입력되어 있다. 이를 가지고 계정을 찾아 state 를 미인증에서 일반 회원으로 변경한다.

package bitcamp.app.controller;

import java.util.UUID;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import bitcamp.app.service.MemberService;
import bitcamp.app.vo.Member;
import bitcamp.util.ErrorCode;
import bitcamp.util.PasswordChecker;
import bitcamp.util.RestResult;
import bitcamp.util.RestStatus;
import jakarta.servlet.http.HttpSession;

@RestController
@RequestMapping("/auth")
public class AuthController {

  Logger log = LogManager.getLogger(getClass());

  {
    log.trace("AuthController 생성됨!");
  }

  @Autowired private MemberService memberService;
  
  @PostMapping("signup")
  public Object signup(
      String nickname,
      String email,
      String password,
      HttpSession session) throws Exception {

    if(nickname.length() <= 50 ||
        email.contains("@") ||
        PasswordChecker.isValidPassword(password)) {

      String token = UUID.randomUUID().toString();
      
      Member member = new Member();
      member.setNickname(nickname);
      member.setEmail(email);
      member.setPassword(password);
      member.setToken(token);

      memberService.add(member);
      
      return new RestResult()
          .setStatus(RestStatus.SUCCESS);
    }

    return new RestResult()
        .setErrorCode(ErrorCode.rest.CONTROLLER_EXCEPTION)
        .setStatus(RestStatus.FAILURE);
  }
  
  @GetMapping("verify")
  public Object verifyEmail(HttpSession session, @RequestParam String token) {
    Member member = memberService.updateByVerifyToken(token);
    
    if (member != null) {
      session.setAttribute("loginUser", member);
      
      return new RestResult()
          .setStatus(RestStatus.SUCCESS);
    } else {
      return new RestResult()
          .setErrorCode(ErrorCode.rest.NO_DATA)
          .setStatus(RestStatus.FAILURE);
    }
  }
  
}

 

 

Service 코드 수정

회원 가입

add() 를 수정한다.

인터페이스는 생략하고 구현체만 설명한다.

기존 memberDao.insert() 밑에 memberDao.updateToken() 을 추가한다.

그리고 아래에 메일을 보낼 설정 및 제목, 본문을 작성하고 메일 전송 메서드를 호출한다.

 

메일 인증 링크 클릭

updateByVerifyToken() 를 추가한다.

memberDao 의 findByToken() 으로 member 객체를 받환받고 객체가 있을 때만 state 를 업데이트 시킨다.

프론트 엔드에서 사용하라고 객체도 리턴해준다.

package bitcamp.app.service.impl;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import bitcamp.app.dao.FollowDao;
import bitcamp.app.dao.MemberDao;
import bitcamp.app.service.MemberService;
import bitcamp.app.vo.Member;
import jakarta.mail.internet.InternetAddress;
import jakarta.mail.internet.MimeMessage;
import jakarta.mail.internet.MimeMessage.RecipientType;

@Service
public class DefaultMemberService implements MemberService {

  Logger log = LogManager.getLogger(getClass());

  @Autowired private MemberDao memberDao;
  @Autowired private FollowDao followDao;
  @Autowired JavaMailSender mailSender;

  @Transactional
  @Override
  public void add(Member member) throws Exception {
    memberDao.insert(member);
    memberDao.updateToken(member);
    
    String receiverMail = member.getEmail();
    MimeMessage message = mailSender.createMimeMessage();
    
    message.addRecipients(RecipientType.TO, receiverMail);// 보내는 대상
    message.setSubject("Artify 회원가입 이메일 인증");// 제목

    String body = "<div>"
                + "<h1> 안녕하세요. Artify 입니다</h1>"
                + "<br>"
                + "<p>아래 링크를 클릭하면 이메일 인증이 완료됩니다.<p>"
                + "<a href='http://localhost:3000/auth/verify?token=" + member.getToken() + "'>인증 링크</a>"
                + "</div>";
    
    message.setText(body, "utf-8", "html");// 내용, charset 타입, subtype
    // 보내는 사람의 이메일 주소, 보내는 사람 이름
    message.setFrom(new InternetAddress("bitcamp1@naver.com", "Artify_Admin"));// 보내는 사람
    mailSender.send(message); // 메일 전송
  }
  
  @Override
  public Member updateByVerifyToken(String token) {
    Member member = memberDao.findByToken(token);
    
    if (member != null) {
      memberDao.updateStateByToken(token);
      return member;
    } else {
      return null;
    }
  }
  
}

 

 

Dao 에 코드 추가

회원 가입

Dao 인터페이스에 updateToken() 메서드 추가한다.

 

메일 인증 링크 클릭

메일 인증 링크를 클릭했을때 사용할 updateStateByToken() 메서드 추가한다.

package bitcamp.app.dao;

import java.util.List;
import java.util.Map;
import org.apache.ibatis.annotations.Mapper;
import bitcamp.app.vo.Member;

@Mapper
public interface MemberDao {

  void updateToken(Member m);
  void updateStateByToken(String token);
  
}

 

 

Mapper 에 코드 추가

MemberDao.xml 에 아래 코드 추가한다.

 

회원 가입

email 을 찾아 token 을 업데이트 한다.

 

메일 인증 링크 클릭

메일 링크 클릭시 state 를 미인증에서 일반 회원 상태로 전환한다.

  <update id="updateToken" parameterType="member">
    update aim_member
    set
      token=#{token}
    where
      email=#{email}
  </update>
  
  <update id="updateStateByToken" parameterType="string">
    update aim_member
    set
      state=0
    where
      token=#{token}
  </update>

 

 

 

테스트

 

가지고 있는 메일로 회원 가입을 해보았다.

인증 메일이 잘온다.

 

인증 링크 클릭하면 인증이 완료되고 로그인 된다.

토큰 값이 틀리면 유효하지 않은 링크라고 뜨고 로그인 시키지 않는다.