[2023.10.09] 임시 비밀번호 발급 구현(SMTP)

아스라이지새는달·2023년 12월 11일
3
post-thumbnail

이번 포스트에서는 비밀번호 찾기(임시 비밀번호 발급) 기능을 구현하면서 알게된 SMTP와 구현방법에 대해서 정리하려고 한다. 구현이 그리 어렵지는 않았지만 나중에도 쓸 것 같은 기능이라 기록해두고 싶었다.
사실 DB에 저장된 비밀번호를 알려주는 기능을 구현하려 했으나 Spring Security에서 복호화 기능을 제공하지 않고 그렇다고 암호화되지 않은 비밀번호를 DB에 저장하는 것은 보안에 위험이 있어 임시 비밀번호를 발급하는 기능으로 구현하였다.

❓ SMTP란

SMTP는 Simple Mail Transfer Protocol의 약자로 인터넷에서 이메일을 보내기 위해 이용되는 프로토콜이다. 메일 서버간의 송수신뿐만 아니라, 메일 클라이언트에서 메일 서버로 메일을 보낼 때에도 사용되는 경우가 많다.

참고로 메일 클라이언트는 Gmail, Outlook 등 사용자가 액세스하여 이메일을 전송하는 컴퓨터나 웹 응용 프로그램을 말하며 SMTP 서버는 SMTP 프로토콜을 사용해 이메일을 전송하고 수신할 수 있는 메일 서버를 말한다.

우리는 이 SMTP를 이용해서 발급된 임시 비밀번호를 멤버의 등록된 이메일로 전송하려한다.


⚙️ 사전 설정

구글 계정 설정

Google 약관이 변경되어 2단계 인증 및 앱 비밀번호 사용을 설정해야 SMTP를 사용 가능하다.

구글 계정에 로그인하고 Google 계정 관리 - 보안 탭에 들어간다.
2단계 인증을 사용해야 앱 비밀번호를 사용할 수 있으므로 2단계 인증을 먼저 설정한다.

2단계 인증을 설정하고 하단으로 내려보면 앱 비밀번호를 설정할 수 있는 버튼이 있다.

App name에 본인이 설정하고 싶은 이름을 입력하고 만들기를 누른다.

만들기 버튼을 누르면 나오는 기기용 앱 비밀번호 16자리 코드를 추후 사용할 예정이기에 저장해둔다.

종속성(Dependency) 추가

build.gradle에 종속성을 추가해준다.

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

application.yml 수정

이메일을 보낼 때 사용하게 될 이메일 계정에 관한 설정을 적는다.
보안상 깃허브에 올릴 때는 .gitignore에 application.yml을 추가하고 올리는 것을 추천한다.

  mail:
    host: smtp.gmail.com 			# 1
    port: 587						# 2
    username: 이메일 계정				# 3
    password: 앱 비밀번호 16자리 코드	# 4
    properties:
      mail:
        smtp:
          auth: true				# 5
          starttls:
            enable: true			# 6
  1. SMTP 서버 호스트 : 구글의 경우 smtp.gmail.com
  2. SMTP 서버 포트 : 구글의 경우 587
  3. SMTP 서버 로그인 아이디 : 이메일
  4. SMTP 서버 로그인 패스워드 : 앱 비밀번호 16자리 코드(공백없이)
  5. 사용자 인증 여부
  6. StartTLS 활성화 여부

StartTLS는 이메일 클라이언트가 TLS 또는 SSL을 사용하여 안전하지 않은 연결에서 안전한 연결로 업그레이드 하려고 함을 이메일 서버에 알리는데 사용되는 프로토콜 명령이다.

  • SSL(Secure Sockets Layer) : 암호화 기반 인터넷 보안 프로토콜
  • TLS(Transport Layer Security) : SSL의 향상된 더욱 안전한 버전

💻 Code

findPwRequestDto

비밀번호 찾기(임시 비밀번호 발급)를 할 때 필요한 정보들을 받는다.

@Getter
@NoArgsConstructor
@AllArgsConstructor
public class findPwRequestDto {

    private Integer studentId;

    private String email;

}

findPwResponseDto

SimpleMailMessage에 담을 내용을 위한 클래스를 선언한다.

@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class findPwResponseDto {

    private String receiveAddress;

    private String mailTitle;

    private String mailContent;

}

사실 역할로 봤을 때는 dto의 기능을 하지 않기 때문에 dto라 명명한 것이 잘못되었지만 추후 수정하는 것으로 하고 넘겼다...

Service

구현하고자 하는 로직에 맞춰 코드를 작성한다.

import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.MailSender;

@Service
@RequiredArgsConstructor
public class MemberServiceImpl implements MemberService {

    private final MemberRepository memberRepository;
    private final PasswordEncoder passwordEncoder;
    private final MailSender mailSender;
    
    @Override
    public String findPw(findPwRequestDto request) throws Exception {
        // request validation
        Member member = memberRepository.findBystudentId(request.getStudentId()).orElseThrow(() ->
                new BadCredentialsException("Invalid Account Information."));

        if(!member.getEmail().equals(request.getEmail())) {
            throw new BadCredentialsException("Email Does Not Match");
        }

        // generate temporary password
        char[] charSet = new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F',
                'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z' };

        StringBuilder tempPw = new StringBuilder();

        for (int i = 0; i < 10; i++) {
            int idx = (int) (charSet.length * Math.random());
            tempPw.append(charSet[idx]);
        }

        // set findPwResponseDto
        findPwResponseDto newDto = findPwResponseDto.builder()
                .receiveAddress(request.getEmail())
                .mailTitle("메일 제목")
                .mailContent("메일 내용")
                .build();

        // send e-mail
        SimpleMailMessage message = new SimpleMailMessage();

        message.setFrom("발송인 이메일 주소");
        message.setTo(newDto.getReceiveAddress());
        message.setReplyTo("회신받을 이메일 주소");
        message.setSubject(newDto.getMailTitle());
        message.setText(newDto.getMailContent());

        mailSender.send(message);

        // set a member's password as a temporary password
        member.updatePassword(passwordEncoder.encode(tempPw));
        memberRepository.save(member);

        return "Temporary password issued.";
    }
}
  1. 사용자에게 입력받은 학번(ID)이 DB에 존재하는지 조회하고 조회한 멤버의 이메일 정보와 입력받은 이메일이 일치하는지 확인한다. 각 과정에서 DB에 존재하지 않거나 일치하지 않으면 Exception을 발생시킨다.
  2. 자바의 Math.random()을 통해 임시 비밀번호를 생성한다.
  3. 보낼 이메일 메시지를 작성하고 mailSender를 통해 발송한다.
  4. 헤당 멤버의 비밀번호를 생성한 임시 비밀번호로 변경한다.

MailSender는 SimpleMailMessage를 작성하여 텍스트 메일을 발송할 수 있고
JavaMailSender는 MimeMessage를 작성하여 HTML로 이루어진 메일을 발송할 수 있다.
필자는 단순히 텍스트를 발송할 예정이라 우선 MailSender와 SimpleMailMessage를 사용하였다.

Controller

Service단에서 Exception을 throw 하였다면 Controller단에서 catch하여 에러 메시지를 출력하도록 하였다.

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/member")
public class MemberController {

    private final MemberService memberService;
    
	@PostMapping("/password")
    private ResponseEntity<String> findPassword(@RequestBody findPwRequestDto request) throws Exception {
        String status;

        try {
            status = memberService.findPw(request);
        } catch(Exception e) {
            e.printStackTrace();
            status = e.getMessage();
        }

        return ResponseEntity.ok().body(status);
    }
}

🧪 테스트

다음과 같이 데이터를 DB에 넣어두고 일어날 수 있는 3가지 경우에 대해 테스트 해보았다.

emailnamepasswordrolestudent_id
oooooooo@naver.comtest3test1234ROLE_STUDENT20231130

email은 필자의 실제 사용 중인 이메일이며 개인정보 보호를 위해 가려두었다.

사용자에게 정상적인 입력을 받았을 때

사용자에게 입력받은 학번(ID)이 DB에 존재하고 조회한 멤버의 이메일 정보와 입력받은 이메일이 같다면 임시 비밀번호를 생성하여 이메일을 발송한 것을 확인할 수 있다.

또한 기존 비밀번호로는 로그인이 되지 않고 발급한 임시 비밀번호로 로그인이 가능한 것을 볼 수 있다.

DB에 존재하는 학번이지만 잘못된 이메일을 입력 받았을 때

설정한 대로 Exception을 발생시키고 설정한 메시지를 출력하는 것을 볼 수 있다.

DB에 존재하지 않는 학번을 입력 받았을 때

마찬가지로 Exception을 발생시키고 설정한 메시지를 출력한다.


🔍 Reference

https://velog.io/@hwaya2828/SMTP

https://mutpp.tistory.com/4

https://www.socketlabs.com/blog/smtp-or-imap/

https://velog.io/@hellocdpa/220319-%EC%9E%84%EC%8B%9C%EB%B9%84%EB%B0%80%EB%B2%88%ED%98%B8-%EC%9D%B4%EB%A9%94%EC%9D%BC%EB%A1%9C-%EB%B3%B4%EB%82%B4%EB%8A%94-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84

https://1-7171771.tistory.com/85

https://velog.io/@tjddus0302/Spring-Boot-%EB%A9%94%EC%9D%BC-%EB%B0%9C%EC%86%A1-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0-Gmail#emailresponsedtojava

profile
웹 백엔드 개발자가 되는 그날까지

0개의 댓글