이번 포스트에서는 비밀번호 찾기(임시 비밀번호 발급) 기능을 구현하면서 알게된 SMTP와 구현방법에 대해서 정리하려고 한다. 구현이 그리 어렵지는 않았지만 나중에도 쓸 것 같은 기능이라 기록해두고 싶었다.
사실 DB에 저장된 비밀번호를 알려주는 기능을 구현하려 했으나 Spring Security에서 복호화 기능을 제공하지 않고 그렇다고 암호화되지 않은 비밀번호를 DB에 저장하는 것은 보안에 위험이 있어 임시 비밀번호를 발급하는 기능으로 구현하였다.
SMTP는 Simple Mail Transfer Protocol의 약자로 인터넷에서 이메일을 보내기 위해 이용되는 프로토콜이다. 메일 서버간의 송수신뿐만 아니라, 메일 클라이언트에서 메일 서버로 메일을 보낼 때에도 사용되는 경우가 많다.
참고로 메일 클라이언트는 Gmail, Outlook 등 사용자가 액세스하여 이메일을 전송하는 컴퓨터나 웹 응용 프로그램을 말하며 SMTP 서버는 SMTP 프로토콜을 사용해 이메일을 전송하고 수신할 수 있는 메일 서버를 말한다.
우리는 이 SMTP를 이용해서 발급된 임시 비밀번호를 멤버의 등록된 이메일로 전송하려한다.
Google 약관이 변경되어 2단계 인증 및 앱 비밀번호 사용을 설정해야 SMTP를 사용 가능하다.
구글 계정에 로그인하고 Google 계정 관리 - 보안 탭에 들어간다.
2단계 인증을 사용해야 앱 비밀번호를 사용할 수 있으므로 2단계 인증을 먼저 설정한다.
2단계 인증을 설정하고 하단으로 내려보면 앱 비밀번호를 설정할 수 있는 버튼이 있다.
App name에 본인이 설정하고 싶은 이름을 입력하고 만들기를 누른다.
만들기 버튼을 누르면 나오는 기기용 앱 비밀번호 16자리 코드를 추후 사용할 예정이기에 저장해둔다.
build.gradle에 종속성을 추가해준다.
implementation 'org.springframework.boot:spring-boot-starter-mail'
이메일을 보낼 때 사용하게 될 이메일 계정에 관한 설정을 적는다.
보안상 깃허브에 올릴 때는 .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
StartTLS는 이메일 클라이언트가 TLS 또는 SSL을 사용하여 안전하지 않은 연결에서 안전한 연결로 업그레이드 하려고 함을 이메일 서버에 알리는데 사용되는 프로토콜 명령이다.
- SSL(Secure Sockets Layer) : 암호화 기반 인터넷 보안 프로토콜
- TLS(Transport Layer Security) : SSL의 향상된 더욱 안전한 버전
비밀번호 찾기(임시 비밀번호 발급)를 할 때 필요한 정보들을 받는다.
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class findPwRequestDto {
private Integer studentId;
private String email;
}
SimpleMailMessage에 담을 내용을 위한 클래스를 선언한다.
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class findPwResponseDto {
private String receiveAddress;
private String mailTitle;
private String mailContent;
}
사실 역할로 봤을 때는 dto의 기능을 하지 않기 때문에 dto라 명명한 것이 잘못되었지만 추후 수정하는 것으로 하고 넘겼다...
구현하고자 하는 로직에 맞춰 코드를 작성한다.
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.";
}
}
MailSender는 SimpleMailMessage를 작성하여 텍스트 메일을 발송할 수 있고
JavaMailSender는 MimeMessage를 작성하여 HTML로 이루어진 메일을 발송할 수 있다.
필자는 단순히 텍스트를 발송할 예정이라 우선 MailSender와 SimpleMailMessage를 사용하였다.
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가지 경우에 대해 테스트 해보았다.
name | password | role | student_id | |
---|---|---|---|---|
oooooooo@naver.com | test3 | test1234 | ROLE_STUDENT | 20231130 |
email은 필자의 실제 사용 중인 이메일이며 개인정보 보호를 위해 가려두었다.
사용자에게 입력받은 학번(ID)이 DB에 존재하고 조회한 멤버의 이메일 정보와 입력받은 이메일이 같다면 임시 비밀번호를 생성하여 이메일을 발송한 것을 확인할 수 있다.
또한 기존 비밀번호로는 로그인이 되지 않고 발급한 임시 비밀번호로 로그인이 가능한 것을 볼 수 있다.
설정한 대로 Exception을 발생시키고 설정한 메시지를 출력하는 것을 볼 수 있다.
마찬가지로 Exception을 발생시키고 설정한 메시지를 출력한다.
https://velog.io/@hwaya2828/SMTP
https://www.socketlabs.com/blog/smtp-or-imap/