게시판 -비밀번호 찾기 및 변경 구현

JIWOO YUN·2024년 5월 9일
0

게시판만들기

목록 보기
15/21
post-thumbnail
post-custom-banner

비밀 번호를 찾기 및 변경을 구현하기 위해서 현재 user 가 회원가입할때 적었던 이메일을 통해서 진행해보자.

참고 블로그 : [Spring Boot] 메일 보내기 구현 (Naver) - JavaMailSender (tistory.com)

mail을 보내기 위해서 springboot에서 제공하는

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

을 추가적으로 gradle에 추가해준다.

spring:
  mail:
    host: smtp.naver.com
    port: 465
    username: 아이디@naver.com
    password: 비밀번호
    properties:
      mail.smtp.auth: true
      mail.smtp.ssl.enable: true
      mail.smtp.ssl.trust: smtp.naver.com
      mail.smtp.starttls.enable: true
  • 메일을 보내는 설정을 위해서 다음과 같은 설정을 yml파일에 설정해준다.
    • 여기서 username과 password의 경우에는 유출이 되면 좋지 않기 때문에 application-secret.yml파일을 추가로 만들어서 위의 설정을 넣어주고 진행하자.

사용할 네이버메일을 SMTP 사용함으로 설정해준다.

  • port 587 로 먼저 진행
    • 587 포트는 SMTP 보안을 사용하는 암호화된 이메일 전송용 포트
      • 587 포트를 통해서 보낼려고하니까 네이버 SMTP가 587이여서 SSL 오류가 발생해서 465로 변경

비밀번호 찾기는 UserService에서 처리해야하기 때문에 UserService에서 이 부분을 처리할 예정

메일을 보내기전에 임시 비밀번호를 생성할 방법을 구상해야한다.

  • 임시비밀번호의 경우 java의 random함수를 사용하려 했으나 random의 경우 난수처럼 보이기 위한 어떤 알고리즘을 사용한 규칙적인 난수로 시드값이 같다면 같은 값이 나오기 때문에 랜덤이 랜덤이 아니다.
    • 이 부분을 보안하기 위해서 secureRandom을 사용했다.
      • secureRandom의 경우 OS에서 임의의 데이터를 가져와서 시드로 사용하기 때문에 더욱 보안에 좋다.
      • 거기에 랜덤은 최대 48 비트만 있지만, secureRandom의 경우 최대 128비트이기 때문에 반복할 가능성도 낮으며 2^128번의 시도가 필요하기 때문에 상대적으로 보안성이 좋다.

redirectAttributes를 통해서 임시비밀번호가 제대로 작성되어 보내지는 경우를 확인하기 위해서 addFlasthAttribute를 통해서 성공을 파악

@PreAuthorize("isAnonymous()")
@PostMapping("/find")
public String findPw(Model model, UserPasswordFindForm userPasswordFindForm
        , BindingResult bindingResult, RedirectAttributes redirectAttributes) {

    if (bindingResult.hasErrors()) {
        return "/user/find";
    }

    SiteUser user = userService.getUser(userPasswordFindForm.getUsername());
    if (user == null) {
        bindingResult.reject("UserNotFound", "일치하는 사용자가 없습니다.");
        return "/user/find";
    }

    if (!user.getEmail().equals(userPasswordFindForm.getEmail())) {
        bindingResult.reject("EmailNotCorrect", "이메일이 일치하지않습니다.");
        return "/user/find";
    }
    String tempPw = userService.SetTempPw(user);

    userService.sendEmail(user.getEmail(), user.getUsername(), tempPw);

    redirectAttributes.addFlashAttribute("successMessage", "임시 비밀번호가 이메일로 전송되엇습니다.");
    return "redirect:/user/login";
}
비밀번호 찾기 html 부분
<html layout:decorate="~{layout}">
<main layout:fragment="content" class="container my-3">
    <form th:action method="post" th:object="${userPasswordFindForm}" onsubmit="FindPasswordForm__submit(this); return false;">
        <div th:replace="~{form_errors :: formErrorsFragment}"></div>
        <h2 class="border-bottom py-2">비밀번호 찾기</h2>
        <div class="mb-3">
            <label lclass="form-label">사용자ID</label>
            <input type="text" th:field="*{username}" class="form-control">
        </div>
        <div class="mb-3">
            <label class="form-label">이메일</label>
            <input type="text" th:field="*{email}" class="form-control">
        </div>
        <button type="submit" class="btn btn-primary">비밀번호찾기</button>
    </form>

    <script th:inline="javascript">
    const emailRegExp = new RegExp(/^[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*@[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*\.[a-zA-Z]{2,3}$/i);

    function FindPasswordForm__submit(form) {
      form.email.value = form.email.value.trim();
      form.username.value = form.username.value.trim();

      if (form.username.value.length  == 0) {
        alert('사용자 아이디를 입력해주세요');
        form.username.focus();
        return false;
      }

      // 이메일 정규식 표현 검사
      if (!emailRegExp.test(form.email.value)) {
        alert('이메일 형식을 지켜야 합니다.');
        form.email.focus();
        return false;
      }

      form.submit();
    }
  </script>
</main>
</html>

javaScript부분은 현재 작성이 제대로 됬는지 확인하기 위한 코드이다.

이메일 유효성 검사의 경우

참고 블로그 : https://taedonn.tistory.com/18

emailRegExp의 경우

@을 기준으로 앞구간이 알파벳 or 숫자 조합인지 확인
@을 기준으로 뒷부분이 알파벳 or 숫자 조합인지 확인
.을 기준으로 뒷붑분이 알파벳 or 숫자 조합으로 이루어져있는지 체크

emailRegExp.test(form.email.value)

  • 문자열의 정규 표현식의 패턴과 일치하는지 알려주는지 불리언 반환
  • test 메서드가 존재

제대로 비밀번호가 날라오는 것을 확인할 수 있다.


  • 비밀번호 변경하기

비밀번호 변경하는 것의 경우 내 정보에서 비밀번호를 변경할 수 있도록 진행

비밀번호 변경 폼

  • 기존 비밀번호
  • 새로운 비밀번호
  • 새로운 비밀번호 확인

으로 구성되면 될 것 같다.

비밀번호 변경 폼에서 확인해야하는 부분

  • 기존 비밀번호가 맞는지
  • 새로운 비밀번호를 적은 내용과 비밀번호 확인 부분이 맞는지 확인이 필요함.
  • 길이는 최소 8자까지 써야한다고 명시할 예정

비밀번호 변경 폼 추가

<html layout:decorate="~{layout}">
<main layout:fragment="content" class="container my-3">
    <form th:action method="post" th:object="${newPwForm}" onsubmit="FindPasswordForm__submit(this); return false;">
        <div th:replace="~{form_errors :: formErrorsFragment}"></div>
        <h2 class="border-bottom py-2">비밀번호 변경</h2>
        <div class="mb-3">
            <label lclass="form-label">기존 비밀번호</label>
            <input type="text" th:field="*{password}" class="form-control">
        </div>
        <div class="mb-3">
            <label lclass="form-label">새로운 비밀번호</label>
            <input type="text" th:field="*{newPassword}" class="form-control">
        </div>
        <div class="mb-3">
            <label lclass="form-label">비밀번호 확인</label>
            <input type="text" th:field="*{CheckPassword}" class="form-control">
        </div>


        <button type="submit" class="btn btn-primary">비밀번호 변경</button>
    </form>

고민했던 부분

  • 비밀번호는 현재 passwordEncoder를 통해서 암호화되어있다.
    • 암호화된 비밀번호를 다시 꺼낸다음 복호화하고 비밀번호가 맞는지 확인하는 걸 Controller단에서 해도 상관없을까가 제일 고민이다.

고민한 부분에대한 해결점 생각

  • 그냥 service에 newPwForm에 적은 비밀번호와 user안에있는 비밀번호를 service에서 비교하고 boolean으로 다르면 false 같으면 true로 뽑자는 생각으로 결론을 냈다.
    • passwordEncoder를 controller에 놓을 필요가 전혀없다고 생각
      • Controller단 보다는 비밀번호를 복호화해서 비교하는 것 자체는 service에서 해야한다고 생각하기 때문에.

passwordEncoder.matches 사용시 주의사항

  • 앞부분에 string 문자열이 와야하고 뒷부분에 암호화된 문자열로와야한다.
public abstract boolean matches(CharSequence rawPassword, String encodedPassword)
  • matches 에 대해서 모르고 사용하다가 계속 뒷부분에 그냥 string 문자열로 넣어서 오류가 나는 것을 보게되었고, 왜 그런거지 하면서 검색하다가 그냥 matches를 타고 들어가보면 답을 알 수 있을 거같아서 들어가보니 메서드 정의가 이렇게 되어있었다.

비밀번호 변경 Controller 코드

@PreAuthorize("isAuthenticated()")
@GetMapping("/changePw")
public String ChangePw(newPwForm newPwForm) {
    return "newPw";
}

@PreAuthorize("isAuthenticated()")
@PostMapping("/changePw")
public String ChangePw(@Valid newPwForm newPwForm, BindingResult bindingResult,
                       Principal principal) {

    SiteUser user = userService.getUser(principal.getName());

    if (bindingResult.hasErrors()) {
        return "newPw";
    }


    log.info("비밀번호가 맞는지 확인");
    if (!userService.isPwMatch(user, newPwForm.getPassword())) {
        bindingResult.reject("PasswordNotCorrect", "비밀번호가 일치하지 않습니다.");
        return "newPw";
    }

    log.info("새로운 비밀번호와 비밀번호 확인이 같은지");
    if (!newPwForm.getNewPassword().equals(newPwForm.getCheckPassword())) {
        bindingResult.reject("NewPasswordInCorrect", "2개의 패스워드가 일치하지않습니다.");
        return "newPw";
    }

    userService.UpdatePw(user, newPwForm.getNewPassword());
    return "redirect:/user/me";
}
  • 문제가 생긴다면 다시 newPw로 이동을 해야하기 때문에 비밀번호가 맞는지와 새로운 비밀번호와 비밀번호가 같은지 확인 진행.

비밀번호가 맞는지 확인 코드

public boolean isPwMatch(SiteUser user, String pw) {
    
    if (passwordEncoder.matches(pw,user.getPassword())) {
        log.info("일치");
        return true;
    }
    return false;
}

비밀번호 변경 코드

@Transactional
public void UpdatePw(SiteUser user, String pw) {
    user.updatePW(passwordEncoder.encode(pw));
}
  • 비밀번호 변경시에는 암호화를 걸어야하기 때문에 encode가 필수로 들어간다.

이렇게 하면 비밀번호 변경까지 진행된다.

profile
열심히하자
post-custom-banner

0개의 댓글