Spring/괴발개발

[괴발개발] REST API - 이메일 인증

오잎 클로버 2022. 3. 3. 08:30
728x90

※본 포스트는 이전 포스트와 이어집니다.※

지난 번에...

가상의 시나리오들 중 3번을 제외한 나머지들은 개발하였습니다.

 

비밀번호 변경을 위한 메일 발송을 하기 위해 몇 가지 세팅을 해야합니다.

준비 과정

먼저 지메일(gmail)를 사용하여 이메일을 발송할 것이기에

앱 비밀번호를 추가해줘야합니다.

앱 비밀번호는 2단계 인증을 사용해야하지만 가능한 기능입니다.

(보안 탭에서 찾을 수 있습니다.)

기기 선택을 메일을 보낼 기기로 선택하고 생성한 후, 도움말에서 나온 대로 설정해주면 사용 가능합니다.

 

그후, dependency를 추가합니다.

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

dependency를 추가하고 난 후에는 application.yml 혹은 properties에 SMTP를 설정합니다.

spring:
    mail:
      host: smtp.gmail.com
      port: 587
      username: {앱 비밀번호를 발급받은 이메일}
      password: {앱 비밀번호}
      properties:
        mail:
          smtp:
            auth: true
            starttls:
              enable: true

이메일 발송 및 유효

이메일을 송신하기 위한 작업이기 때문에 회원가입 코드를 수정하였습니다.

javax.mail.internet.* 에 있는 라이브러리를 사용하여 해당 이메일이 유효한 이메일인지 판단하도록 하였습니다.

@RequiredArgsConstructor
@Service
public class MailService {

    private final JavaMailSender javaMailSender;
    private final SpringTemplateEngine templateEngine;

    @Value("${spring.mail.username}")
    private String senderEmail;

    public void sendMail(Long id, String email) {
        try {
            MimeMessage mimeMailMessage = javaMailSender.createMimeMessage();
            MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(mimeMailMessage, true);
            mimeMessageHelper.setSubject("[Workshop6349 Security]");
            mimeMessageHelper.setFrom(senderEmail);
            mimeMessageHelper.setTo(email);

            Context context = new Context();
            context.setVariable("id", id);

            String html = templateEngine.process("emailTemplate", context);
            mimeMessageHelper.setText(html, true);

            javaMailSender.send(mimeMailMessage);
        } catch (MessagingException e) {
            e.printStackTrace();
        }
    }

    public boolean isValidEmailAddress(String email) {
        boolean result = true;
        try {
            InternetAddress emailAddress = new InternetAddress(email);
            emailAddress.validate();
        } catch (AddressException e) {
            result = false;
        }

        return result;
    }
    
}

그리고 이메일을 발송할 수 있도록 service 클래스를 생성하였습니다.

※ 주의! isValidEmailAddress 메소드는 해당 이메일의 존재 유무는 알지 못합니다. 단지, 정규표현식으로 하여 올바른 이메일 방식인지 유무를 묻는 것 뿐입니다. ※

private final MailService mailService;

@Transactional
public void register(UserRequestDto userRequestDto) {
    String email = userRequestDto.getEmail();
    String password = userRequestDto.getPassword();

    if (mailService.isValidEmailAddress(email)) {
        String msg = email + " is not valid email!";
        throw new HttpClientErrorException(HttpStatus.BAD_REQUEST, msg);
    }

    if (userRepository.existsByEmail(email)) {
        String msg = "user " + email + " is already exists";
        throw new HttpClientErrorException(HttpStatus.CONFLICT, msg);
    }

    String salt = securityService.getSalt();
    String hashedPassword = securityService.encrypt(password + salt);

    userRepository.save(User.builder()
                    .email(email)
                    .password(hashedPassword)
                    .salt(salt)
            .build());
}

public void sendChangePasswordEmail(String token) {
    UserResponseDto userResponseDto = findByToken(token);
    mailService.sendMail(userResponseDto.getId(), userResponseDto.getEmail());
}

현재 비밀번호 변경은 AccessToken이 있어야지만 변경가능하도록 설계되어 있습니다.

AccessToken없이도 변경하도록 하는 방법은 회원가입 코드를 응용하여

  1. 이메일이 유효한지 확인
  2. 회원으로 저장되어 있는 지 확인
  3. 해당 이메일로 비밀번호 변경 이메일 전송
@RequiredArgsConstructor
@Controller
public class EmailController {

    private final UserService userService;
    private final SecurityService securityService;

    @GetMapping("/change-password/{id}")
    public ModelAndView passwordChangeDirect(@PathVariable Long id) {
        ModelAndView modelAndView = new ModelAndView();
        modelAndView.setViewName("passwordTemplate");
        UserResponseDto userResponseDto = userService.findById(id);
        modelAndView.addObject("email", userResponseDto.getEmail());
        return modelAndView;
    }

    @PostMapping("/password")
    public String changePassword(String email, String password) {
        User user = userService.findByEmail(email);
        String salt = securityService.getSalt(); // salt 도 변경
        if (user.setPassword(securityService.encrypt(password + salt))) {
            return "passwordChangeSuccess";
        }
        return "passwordChangeFailed";
    }

}

html (타임리프 엔진)

templates.zip
0.00MB

테스트

유효한 이메일로 회원가입 시도시
이상한 방식의 이메일로 회원가입을 요청할 경우
허용된 액세스 토큰으로 비밀번호 변경 요청을 할 경우
이메일로 비밀번호 변경하기 - 아래 버튼을 누를 시 아래 사진으로 이동
비밀번호 변경
비밀번호가 이전 비밀번호와 달라 성공적으로 변경된 경우
비밀번호가 현재 비밀번호와 동일하여 변경에 실패한 경우

 

 

이상입니다.