Spring Boot/STUDY

[Spring Boot] JPA 프로젝트 - 비밀번호 찾기 및 변경 기능 구현(2)

코맹 2024. 6. 28. 11:23

 

 

 

[Spring Boot] JPA 프로젝트 - 비밀번호 찾기 및 변경 기능 구현(1)

비밀번호 찾기 및 변경 기능을 하기 전 메일 전송 테스트를 해볼려고 한다! build.gradle 메일을 보내기 위한 디펜던시 추가// 메일전송 디펜던시implementation 'org.springframework.boot:spring-boot-starter-mail' 

iieunji023.tistory.com

 

비밀번호 기능 구현 전 메일 테스트까지 완료했다

 

 

/templates/member/login.html 비밀번호 초기화 버튼
<a href="/member/reset" class="btn btn-sm btn-info">비밀번호 초기화</a>
  • <a> 태그를 통해 비밀번호 초기화시 이동될 url 연결

 

/controller/MemberController.java reset() 메서드 추가
  @GetMapping("/reset")
  public String reset() {
    return "member/reset";    // /templates/member/reset.html
  }
  • http://localhost:8080/member/reset 접속시 reset.html 화면을 띄움

 

/templates/member/reset.html 생성 -> register.html 가져와서 수정
<!doctype html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org" layout:decorate="~{layout}">
  <div layout:fragment="main-content" class="card container my-3 form-register">
    <div class="my-3 border-bottom">
      <h4>비밀번호 초기화</h4>
    </div>
    <form th:action="@{/mail/reset-mail}" method="post" >
      <div class="mb-3">
        <label for="email" class="form-lable">이메일</label>
        <input type="email" id="email" name="email" class="form-control" required>
      </div>

      <div class="d-flex justify-content-center">
        <button type="submit" class="btn btn-sm btn-primary mx-2">초기화</button>
        <a href="/" class="btn btn-sm btn-secondary">취소</a>
      </div>
    </form>
  </div>
</html>

 

💥💥💥

어제 postman에서 restful API 테스를 위해 CSRF를 꺼놨었다.
오늘 로그인을 하고 게시글을 등록하니 CSRF관련 오류가 발생했다.
알고보니 create.html과 modify.html에 적어두었던 CSRF토큰 null 오류였다.

따라서, /board/create.html, /reply/modify.html에 있는 CSRF 관련 태그 주석처리해야 함!
<!-- <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" /> -->​

 

/controller/MailController.java 생성, /mail/reset-mail GET매핑 메서드 생성
  • MailController.java를 만들기 전...
    • Member엔티티에서 email을 가져오는 메서드 없기 때문에 이메일 주소를 가져올 수 있는 메서드를 추가해야한다.
    • 🔽 /service/MemberService.java에 메일 주소로 검색하는 메서드 getMemberByEmail() 추가
  // 24.06.28 이메일로 사용자 검색 메서드
  public Member getMemberByEmail(String email){
    Optional<Member> member = this.memberRepository.findByEmail(email);
    if(member.isPresent()) 
      return member.get();
    else 
      throw new NotFoundException("Member not found!");

  }

 

  • /controller/MailController.java 생성, /mail/reset-mail GET매핑 메서드 생성
package com.eunji.backboard.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

import com.eunji.backboard.entity.Member;
import com.eunji.backboard.service.MailService;
import com.eunji.backboard.service.MemberService;

import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.web.bind.annotation.PostMapping;


@RequestMapping("/mail")
@RequiredArgsConstructor
@Controller
@Log4j2
public class MailController {
  private final MemberService memberService;    // 회원가입한 아이디의 메일주소를 확인하기 위함
  private final MailService mailService;

  @PostMapping("/reset-mail")
  public String reset_mail(Model model, @RequestParam("email") String email) {
    log.info(String.format("▶▶▶ reset.html에서 넘어온 이메일: %s", email));

    /*
     * DB에서 메일주소가 있는지 확인
     * 있으면 초기화 메일 보내고
     * 없으면 에러
     */
      try{
        Member member = this.memberService.getMemberByEmail(email);

        // 메일 전송
        Boolean result = this.mailService.sendResetPasswordEmail(member.getEmail());

        if(result) {
          log.info("▶▶▶ 초기화 메일 전송 완료!!");
          model.addAttribute("result", "초기화 메일 전송 성공!");

        } else {
          model.addAttribute("result", "초기화 메일 전송 실패! 관리자에게 문의하세요.");

        }

      } catch(Exception e) {
        model.addAttribute("result", "초기화 메일 전송 실패! 사용자가 없습니다.");

      }
      return "member/reset_result";   // /templates/member/reset_result.html 파일

  }
  
}

 

💥 💥 💥

  • restcontroller에 만들었던 MailController.java와 이름 동일 문제로 에러 발생
  • restcontroller/ MailController -> RestMailController.java로 이름 변경

 

/templates/member/reset_result.html 생성
<!doctype html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org" layout:decorate="~{layout}">
  <div layout:fragment="main-content" class="card container my-3 form-register">
    <div class="my-3 border-bottom">
      <h4>비밀번호 초기화</h4>
    </div>
      <div class="mb-3">
        <div th:text="${result}"></div>
      </div>

      <div class="d-flex justify-content-center">
        <a href="/" class="btn btn-sm btn-secondary">Home</a>
      </div>
  </div>
</html>

 

/service/MailService.java에 메일전송 메서드 생성, 수정
  // 메일에서 초기화할 화면으로 이동 URL
  private String resetPassUrl = "http://localhost:8080/member/reset-password";
  
  
  // 패스워드 초기화 메일 전송 METHOD
  public Boolean sendResetPasswordEmail(String email) {
    String subject = "요청하신 비밀번호 재설정입니다.";
    String message = "BackBoard"
                  +  "<br><br>" + "아래 링크를 클릭하면 비밀번호 재설정 페이지로 이동합니다." + "<br>"
                  +  "<a href='" + resetPassUrl + "/" + email + "'>"
                  +  resetPassUrl + "/" + email + "</a>" + "<br><br>";

    try{
      sendMail(email, subject, message);
      return true;

    }catch(Exception e) {
      return false;

    }
  }

 

  • sendMail() 메서드 수정
mmh.setText(message, true);

 

  • UUID를 생성해서 메일로 전송하는 메서드 추가
// 중복되지 않는 ID생성
  private String makeUuid() {
    return UUID.randomUUID().toString();
  }

 

  • sendResetPasswordEmail() 메서드 수정
  public Boolean sendResetPasswordEmail(String email) {
    String uuid = makeUuid();
    String subject = "요청하신 비밀번호 재설정입니다.";
    String message = "BackBoard"
                  +  "<br><br>" + "아래 링크를 클릭하면 비밀번호 재설정 페이지로 이동합니다." + "<br>"
                  +  "<a href='" + resetPassUrl + "/" + uuid + "'>"
                  +  resetPassUrl + "/" + uuid + "</a>" + "<br><br>";

    try{
      sendMail(email, subject, message);
      return true;

    }catch(Exception e) {
      return false;

    }
  }

비밀번호 초기화 클릭시 받은 메일

  • resetPassUrl 뒤에 UUID가 붙는 것을 확인할 수 있음

 

/entity/Reset.java 생성
package com.eunji.backboard.entity;

import java.time.LocalDateTime;

import org.springframework.data.annotation.CreatedDate;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Entity
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Reset {
  @Id
  @GeneratedValue(strategy = GenerationType.SEQUENCE)
  private Integer id;

  private String email;

  private String uuid;

  @CreatedDate
  @Column(updatable = false)
  private LocalDateTime regDate;
  
}

 

  • h2-console에서 reset 테이블이 만들어진 것을 확인할 수 있음

 

/repository/ResetRepository.java 인터페이스 생성, findByUuid() 추가
package com.eunji.backboard.repository;

import java.util.Optional;

import org.springframework.data.jpa.repository.JpaRepository;

import com.eunji.backboard.entity.Reset;

public interface ResetRepository extends JpaRepository<Reset, Integer>{
  Optional<Reset> findByUuid(String uuid);  // UUID 테이블 검색
  
}

 

/service/ResetService.java 생성
package com.eunji.backboard.service;

import java.time.LocalDateTime;
import java.util.Optional;

import org.springframework.stereotype.Service;

import com.eunji.backboard.common.NotFoundException;
import com.eunji.backboard.entity.Reset;
import com.eunji.backboard.repository.ResetRepository;

import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;

@RequiredArgsConstructor
@Service
@Log4j2
public class ResetService {
  private final ResetRepository resetRepository;

  public void setReset(String uuid, String email) {
    Reset reset = Reset.builder().uuid(uuid).email(email).regDate(LocalDateTime.now()).build();

    this.resetRepository.save(reset);
    log.info("⏺⏺⏺ setReset() 성공!!!" );

  }

  public Reset getReset(String uuid) {
    Optional<Reset> _reset = this.resetRepository.findByUuid(uuid);

    if(_reset.isPresent()) {
      log.info("⏺⏺⏺ getReset() 데이터있음!!!" );
      return _reset.get();

    } else {
      throw new NotFoundException("Reset not found!");
    }

  }
  
}

 

/service/MailService.java에 ResetService 객체 생성, 메일 전송 후 setReset()을 사용하여 DB에 insert하기
private final ResetService resetService;    // ResetService는 예외

  // 패스워드 초기화 메일 전송 METHOD(!!!)
  @Transactional
  public Boolean sendResetPasswordEmail(String email) {
    String uuid = makeUuid();
    String subject = "요청하신 비밀번호 재설정입니다.";
    String message = "BackBoard"
                  +  "<br><br>" + "아래 링크를 클릭하면 비밀번호 재설정 페이지로 이동합니다." + "<br>"
                  +  "<a href='" + resetPassUrl + "/" + uuid + "'>"
                  +  resetPassUrl + "/" + uuid + "</a>" + "<br><br>";

    try{
      sendMail(email, subject, message);
      saveUuidAndEmail(uuid, email);

      return true;

    }catch(Exception e) {
      return false;

    }
  }

 // uuid 정보를 db에 넣기 위한 method
  private void saveUuidAndEmail(String uuid, String email) {
    this.resetService.setReset(uuid, email);
  }
  • 변경된 부분
  • 메일전송후 생성된 UUID를 DB에 저장하기 위해 MailService에서 ResetService 객체를 생성
    (일종의 예외처리)
  • ⭐ 트랜잭션 공부하기⭐

 

/controller/MemberController.java, /mail/reset-password GET매핑 메서드 작성
  @GetMapping("/reset-password/{uuid}")
  public String reset_password(MemberForm memberForm, @PathVariable("uuid") String uuid) {
    Reset reset = this.resetService.getReset(uuid);
    log.info(String.format("▶▶▶ 확인된 이메일: [%s]", reset.getEmail()));

    // /reset-password/{uuid}로 들어갔을 때 사용자 이름과 이메일을 가져오기 위함
    Member member = this.memberService.getMemberByEmail(reset.getEmail());
    memberForm.setUsername(member.getUsername());
    memberForm.setEmail(member.getEmail());

    return "member/newpassword";
  }

 

/templates/member/newpassword.html 생성
<!doctype html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org" layout:decorate="~{layout}">
  <div layout:fragment="main-content" class="card container my-3 form-register">
    <div class="my-3 border-bottom">
      <h4>비밀번호 초기화</h4>
    </div>
    <form th:action="@{/member/reset-password}" th:object="${memberForm}" method="post" >
      <!-- /templates/errors.html -->
      <div th:replace="~{errors :: formErrorFragment}"></div>
      <div class="mb-3">
        <label for="username" class="form-lable">사용자이름</label>
        <input type="text" th:field="*{username}" class="form-control bg-light" readonly>
      </div>
      <div class="mb-3">
        <label for="email" class="form-lable">이메일</label>
        <input type="email" th:field="*{email}" class="form-control bg-light" readonly>
      </div>
      <div class="mb-3">
        <label for="password1" class="form-lable">비밀번호</label>
        <input type="password" th:field="*{password1}" class="form-control">
      </div>
      <div class="mb-3">
        <label for="password2" class="form-lable">비밀번호 확인</label>
        <input type="password" th:field="*{password2}" class="form-control">
      </div>

      <div class="d-flex justify-content-center">
        <button type="submit" class="btn btn-sm btn-primary mx-2">비밀번호 초기화</button>
        <a href="/" class="btn btn-sm btn-secondary">취소</a>
      </div>
    </form>
  </div>
</html>
  • disabled를 사용하면 안됨! 사용하지 않으므로 값이 안넘어감
  • disabled -> readonly로 변경

 

사용자이름, 이메일 값 들어오는 것 확인

 

/controller/MemberController.java, /mail/reset-password POST매핑 메서드 작성
  @PostMapping("/reset-password")
  public String reset_password(@Valid MemberForm memberForm, BindingResult bindingResult) {
    if(bindingResult.hasErrors()) {
      return "member/newpassword";

    }

    if(!memberForm.getPassword1().equals(memberForm.getPassword2())) {
      bindingResult.rejectValue("password2", "passwordInCorrect", "패스워드가 일치하지 않습니다.");

      return "member/newpassword";

    }

    Member member = this.memberService.getMember(memberForm.getUsername()); // 현재 사용자 정보 가져오기
    member.setPassword(memberForm.getPassword1());  // 패스워드 변경

    this.memberService.setMember(member);   // 업데이트

    return "redirect:/member/login";
      
  }

 

/service/MemberService.java setMember() 메서드 생성
  //  비밀번호 업데이트 후 db에 저장하기 위한 메서드 - 기존 사용자 비번 초기화
  public void setMember(Member member) {
    member.setPassword(passwordEncoder.encode((member.getPassword())));    // 암호화한 값을 DB에 저장
    this.memberRepository.save(member);   // 업데이트
  }
  • 암호화 꼭 해줘야 함!