멤버쉽/이메일 인증

jskim2x·2023년 9월 11일

프로젝트

목록 보기
5/7

기존 졸업작품의 로그인 방식?

Id/Pw 로그인을 적용했었다

  • 하지만 요새 앱들을 보면 대부분 email, social login(카카오 로그인..) 방식을 취한다

왜 그렇게 되었을까?

요즘의 고객들을 비즈니스로 끌어오려면, 과정의 단순함은 필수라고 한다.

  • Social Login으로 회원가입/로그인 과정을 단순히 하되
  • Social Login에 익숙치 않은 분들을 위해 이메일 로그인 방식을 남겨놓는다~ 라고는 들었지만.. 이는 정확하진 않다! 알아봐야겠다.

사전준비

  • 아직 prod 환경(서버)를 구축해두지 않았기 때문에, 현재상태로 깃허브에 푸시했다간 flow error(Github Actions)가 나올 것 같아서 올리진 않은 상태이다.
  • 로컬에서는 초기작업을 마친 상태.

서비스 로직에서 인증번호를 생성하고, 이메일을 보내는 것을 구현했다.

  • Gmail을 기준으로 작성했다.
  • 아마 naver, nate등과 같은 메일을 통해 보내는 것도 쉽게 가능하다고 생각한다.

1. 구글 계정 설정


2022년 5월 기준, 보안 수준이 낮은 앱의 액세스 설정이 불가능하게 되었다고 한다. 따라서, 다음과정을 통해 앱 비밀번호를 생성해야 한다.
1. 구글 로그인

  • 계정관리에 들어간 다음

2. 보안 > 2단계 인증

3. 맨 밑 하단의 앱 비밀번호

  • 나 같은 경우는 이미 만들었기 때문에 있다.
  • 들어가보면 기기를 선택하라고 나올 것이다.

4. 앱 선택, 기기 선택

  • "앱 선택" -> "메일"
  • "기기 선택" -> "Windows 컴퓨터"
  • 위의 두 설정을 마무리하고 생성하면
  • 이런식으로, 16자리의 비밀번호가 나오게 되는데, 이는 프로젝트에서 메일을 발송할 주체를 등록할 때 사용된다.

구현

1. Dependency 추가

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

2. EmailConfig

package com.uou.capstone.global.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 java.util.Properties;

@Configuration
public class EmailConfig {
    @Value("${mail.smtp.port}")
    private int port;

    @Value("${mail.smtp.socketFactory.port}")
    private int socketPort;

    @Value("${mail.smtp.auth}")
    private boolean auth;

    @Value("${mail.smtp.starttls.enable}")
    private boolean starttls;

    @Value("${mail.smtp.starttls.required}")
    private boolean starttls_required;

    @Value("${mail.smtp.socketFactory.fallback}")
    private boolean fallback;

    @Value("${adminMail.id}") // 발송자 이메일
    private String id; 

    @Value("${adminMail.password}") // 발송자 이메일의 앱 비밀번호
    private String password;
    @Bean // Bean을 주입시켜서, 다른 서비스 로직에서 이메일을 보내기 위해 사용하기 위함이다.
    public JavaMailSender javaMailService() {
        JavaMailSenderImpl javaMailSender = new JavaMailSenderImpl();
        // 이메일 서버의 호스트를 설정
        javaMailSender.setHost("smtp.gmail.com");
        
        // 이메일 서버에 인증하기 위해 사용자 이름과 비밀번호 설정
        // 이메일을 발송할 사람을 인증하는 것임
        javaMailSender.setUsername(id);
        javaMailSender.setPassword(password);
        javaMailSender.setPort(port); // SMTP 서버의 포트를 설정
        javaMailSender.setJavaMailProperties(getMailProperties()) // 추가 메일 속성;
        javaMailSender.setDefaultEncoding("UTF-8");
        return javaMailSender;
    }
    // 메일 세션 설정에 필요한 Java 메일 속성들을 설정
    private Properties getMailProperties() {
        Properties pt = new Properties();
        pt.put("mail.smtp.socketFactory.port", socketPort);
        pt.put("mail.smtp.auth", auth);
        pt.put("mail.smtp.starttls.enable", starttls);
        pt.put("mail.smtp.starttls.required", starttls_required);
        pt.put("mail.smtp.socketFactory.fallback", fallback);
        pt.put("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory"); // SSLSocketFactory를 사용하여 SSL 연결을 위한 소켓 팩토리를 설정
        return pt;
    }
}
  • @Value에서 참조하는 변수들은 yml에서 가져오는 값들이고, 이들을 바로 밑의 변수들에게 주입시켜준다는 의미이다.

3. application-dev.yml

mail:
  smtp:
    port: 587
    socketFactory:
      port: 465
      fallback: true
    auth: true
    starttls:
      enable: true
      required: true

adminMail:
  id: youremail@gmail.com # 여기 본인 메일주소
  password: yourpassword # 본인 앱 비밀번호
  • mail.smtp.port : SMTP 서버의 포트를 설정한다. 587은 일반적으로 SMTP 서버를 위해 사용되는 포트 중 하나이다.
  • mail.smtp.socketFactory.port : 소켓 팩토리를 위한 포트를 설정. 465는 SSL을 통한 SMTP 통신을 위해 일반적으로 사용되는 포트이다.
  • mail.smtp.socketFactory.fallback : 소켓 팩토리 연결이 실패한 경우 폴백(fallback) 연결을 사용할지 여부를 지정한다.
  • mail.smtp.auth : SMTP 인증을 활성화한다.
  • mail.smtp.starttls.enable : STARTTLS를 활성화하여 통신 경로를 암호화한다.
  • mail.smtp.starttls.required : STARTTLS 사용이 필수적인지 지정한다. 이는 메일 서버가 STARTTLS를 지원해야 메일 전송이 가능함을 나타낸다.
  • adminMail.id : 이메일 서비스에서 사용할 관리자의 이메일 ID를 설정한다.
  • adminMail.password : 이메일 서비스에서 사용할 관리자의 이메일 비밀번호를 설정한다. 필자는 구글 계정의 앱 비밀번호를 사용했다.

4. EmailService

package com.uou.capstone.global.config;

public interface EmailService {
    String sendSimpleMessage(String to)throws Exception;
}
  • EmailService를 interface로 둔 이유는 OOPdml 다형성을 충족하기 위함이다.
  • 예를 들어, 키보드를 치는 행위를 서비스로 구현했다면, KeyboardService라는 interface를 구현하고 이를 implement하여 A를 치는 행위를 구현한 클래스는 AkeyboardService, B를 치는 행위를 구현한 클래스는 BkeyboardService로 역할을 나누는 것이다.
  • 지금 상황에서는 확장성을 고려한 설계라고 볼 수 있다.

5. UserService

package com.uou.capstone.domain.app.user.service;

import com.fasterxml.jackson.databind.ser.Serializers;
import com.uou.capstone.domain.app.user.dto.PostAuthEmailBeforeReq;
import com.uou.capstone.domain.app.user.dto.PostAuthEmailBeforeRes;
import com.uou.capstone.global.config.EmailService;
import com.uou.capstone.global.config.error.exception.BaseException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.mail.MailException;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.stereotype.Service;

import javax.mail.Message;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import java.util.Random;

@Service
@Slf4j
@RequiredArgsConstructor
public class UserService implements EmailService {
    private final JavaMailSender emailSender;
    public PostAuthEmailBeforeRes emailcheckBefore(PostAuthEmailBeforeReq postAuthEmailBeforeReq) throws BaseException {
        try{
            sendSimpleMessage(postAuthEmailBeforeReq.getEmail());
        }catch (Exception e){
            e.printStackTrace();
        }
        return new PostAuthEmailBeforeRes("Success");
    }
    public static final String ePw = createKey();

    private MimeMessage createMessage(String to)throws Exception{
        System.out.println("보내는 대상 : "+ to);
        System.out.println("인증 번호 : "+ePw);
        MimeMessage  message = emailSender.createMimeMessage();

        message.addRecipients(Message.RecipientType.TO, to);//보내는 대상
        message.setSubject("이메일 인증 테스트");//제목

        String msgg="";
        msgg+= "<div style='margin:20px;'>";
        msgg+= "<h1> 안녕하세요 김지섭입니다. </h1>";
        msgg+= "<br>";
        msgg+= "<p>아래 코드를 복사해 입력해주세요<p>";
        msgg+= "<br>";
        msgg+= "<p>감사합니다.<p>";
        msgg+= "<br>";
        msgg+= "<div align='center' style='border:1px solid black; font-family:verdana';>";
        msgg+= "<h3 style='color:blue;'>회원가입 인증 코드입니다.</h3>";
        msgg+= "<div style='font-size:130%'>";
        msgg+= "CODE : <strong>";
        msgg+= ePw+"</strong><div><br/> ";
        msgg+= "</div>";
        message.setText(msgg, "utf-8", "html");//내용
        message.setFrom(new InternetAddress("이메일","kimjiseop"));//보내는 사람

        return message;
    }

    public static String createKey() {
        StringBuffer key = new StringBuffer();
        Random rnd = new Random();

        for (int i = 0; i < 8; i++) { // 인증코드 8자리
            int index = rnd.nextInt(3); // 0~2 까지 랜덤

            switch (index) {
                case 0:
                    key.append((char) ((int) (rnd.nextInt(26)) + 97));
                    //  a~z  (ex. 1+97=98 => (char)98 = 'b')
                    break;
                case 1:
                    key.append((char) ((int) (rnd.nextInt(26)) + 65));
                    //  A~Z
                    break;
                case 2:
                    key.append((rnd.nextInt(10)));
                    // 0~9
                    break;
            }
        }
        return key.toString();
    }
    @Override
    public String sendSimpleMessage(String to)throws Exception {
        // TODO Auto-generated method stub
        MimeMessage message = createMessage(to);
        try{//예외처리
            emailSender.send(message);
        }catch(MailException es){
            es.printStackTrace();
            throw new IllegalArgumentException();
        }
        return ePw;
    }
}
  • 이메일로 보낼 인증번호와 html형식이라고 생각하면 된다

6. UserController

package com.uou.capstone.domain.app.user;

import com.uou.capstone.domain.app.user.dto.PostAuthEmailBeforeReq;
import com.uou.capstone.domain.app.user.dto.PostAuthEmailBeforeRes;
import com.uou.capstone.domain.app.user.service.UserService;
import com.uou.capstone.global.config.error.BaseResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/app")
public class UserController {
    private final UserService userService;

    @PostMapping("/users/auth/email")
    public ResponseEntity<BaseResponse<PostAuthEmailBeforeRes>> emailCheckBefore(@RequestBody PostAuthEmailBeforeReq postAuthEmailBeforeReq){
        PostAuthEmailBeforeRes postAuthEmailBeforeRes = userService.emailcheckBefore(postAuthEmailBeforeReq);
        return ResponseEntity.ok(new BaseResponse<>(postAuthEmailBeforeRes));
    }

}

7. Dto

PostAuthEmailBeforeReq.java

package com.uou.capstone.domain.app.user.dto;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class PostAuthEmailBeforeReq {
    private String email;
}

PostAuthEmailBeforeRes.java

package com.uou.capstone.domain.app.user.dto;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class PostAuthEmailBeforeRes {
    private String authEmailCheck;
}

8. API Test

9. 메일 확인


1. 솔직히, 참고 블로그 하나로 구현을 완성할 정도로 난이도가 어렵지는 않았다. 중요한건 원리인 것 같다. SMTP 프로토콜에 대해서 공부해봐야겠다.

2. 응답을 보면 알겠지만, 협업할 때 저런 응답 값을 보내주면 프론트분들께 욕먹을게 분명하다

3. 어차피 안드로이드 프로젝트를 가져와서 서비스를 완성시켜야 하기 때문에, 일단은 저렇게 냅둔다

참고 블로그

0개의 댓글