회원 관련 기능
회원 관련 기능들을 계속해서 구현 중에 있다. 우리가 웹 사이트에 가입했는데 시간이 지나 다시 접속하려고 했을 때, 비밀번호를 까먹었던 경험...😲 한번쯤은 있을 것이다. 그럴 때를 대비해 웹에서는 가입한 회원이 자신의 이메일을 입력했을 때, 입력한 이메일로 임시 비밀번호를 전송하는 기능을 갖추고 있다. 그 기능을 한 번 구현해보았다.
hellocdpa 님의 벨로그 를 많이 참조했다. 감사합니다 (꾸벅)
먼저, Mail 관련 Dto를 생성한다.
MailDto.java
package com.shop.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class MailDto {
private String address;
private String title;
private String message;
}
MemberController에 다음과 같은 RequestMapping 클래스들을 추가한다.
MemberController.java
package com.shop.controller;
// ..import 생략
@RequestMapping("/members")
@Controller
@RequiredArgsConstructor
public class MemberController {
// ...@Autowired 생략
// 회원 비밀번호 찾기
@GetMapping(value = "/findMember")
public String findMember(Model model) {
return "/member/findMemberForm";
}
// 비밀번호 찾기시, 임시 비밀번호 담긴 이메일 보내기
@Transactional
@PostMapping("/sendEmail")
public String sendEmail(@RequestParam("memberEmail") String memberEmail){
MailDto dto = mailService.createMailAndChangePassword(memberEmail);
mailService.mailSend(dto);
return "/member/memberLoginForm";
}
첫 번째로 비밀번호를 찾기 위해 /findMember
url에 접속하면 비밀번호를 찾을 수 있는 폼을 보여주기 위해 findMember
메서드를 생성한다.
그리고 view 단에서 비밀번호 찾기 전송 시도를 했을 때, 실제로 mailService
에서 Mail을 전송하고 비밀번호를 변경하게끔 하는 Controller도 필요하다. 이것은 sendEmail
이라는 클래스로 구현하는데, 이 때 @RequestParam 을 통해 view로부터 실제 사용자가 입력한 이메일을 "memberEmail" 라는 파라미터로 가져올 수 있게 한다. mailService
에 관한 내용은 이 후, 생성해줄 것이다.
이 모든 내용은 @Transactional
선언적 트랜잭션을 통해 dirty checking을 하게되고, save를 하지 않아도 수정된 사항이 반영될 것이다.
이제 Service 단에서 구현할 차례다. 실제 입력 받은 이메일을 토대로 임시 비밀번호 메일을 전송하고 더불어 임시 비밀번호로 회원 비밀번호를 변경하는 작업을 할 수 있게끔 한다. MemberService
에서 구현해도 되지만 전에 이메일 인증기능을 구현하면서 만들어놓았던 MailService
를 활용하기로 했다.
MailService.java
package com.shop.service;
//..import 생략
@Service
@RequiredArgsConstructor
public class MailService {
// 임시 비밀번호 생성
public static String getTempPassword(){
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' };
String str = "";
// 문자 배열 길이의 값을 랜덤으로 10개를 뽑아 구문을 작성함
int idx = 0;
for (int i = 0; i < 10; i++) {
idx = (int) (charSet.length * Math.random());
str += charSet[idx];
}
return str;
}
// 메일 내용을 생성하고 임시 비밀번호로 회원 비밀번호를 변경
public MailDto createMailAndChangePassword(String memberEmail) {
String str = getTempPassword();
MailDto dto = new MailDto();
dto.setAddress(memberEmail);
dto.setTitle("댕댕월드 임시비밀번호 안내 이메일 입니다.");
dto.setMessage("안녕하세요. 댕댕월드 임시비밀번호 안내 관련 이메일 입니다." + " 회원님의 임시 비밀번호는 "
+ str + " 입니다." + "로그인 후에 비밀번호를 변경해주세요!");
updatePassword(str, memberEmail);
return dto;
}
// MailDto를 바탕으로 실제 이메일 전송
public void mailSend(MailDto mailDto) {
System.out.println("전송 완료!");
SimpleMailMessage message = new SimpleMailMessage();
message.setTo(mailDto.getAddress());
message.setSubject(mailDto.getTitle());
message.setText(mailDto.getMessage());
message.setFrom("wisejohn950330@gmail.com");
message.setReplyTo("wisejohn950330@gmail.com");
System.out.println("message"+message);
javaMailSender.send(message);
}
//임시 비밀번호로 업데이트
public boolean updatePassword(String str, String email){
try {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
String encodePw = encoder.encode(str); // 패스워드 암호화
Member member = memberRepository.findByEmail(email);
member.updatePassword(encodePw);
memberRepository.save(member);
return true;
} catch (Exception e) {
return false;
}
}
}
각 메서드를 살펴보면...
getTempPassword()
: 난수 생성 라이브러리를 통해 10개의 무작위 숫자 or 문자 조합으로 10자리 임시 비밀번호를 생성한다.
createMailAndChangePassword
: 새로운 메일 객체를 생성하고, 실제 전송할 MailDto에 포함될 내용(Title, Message 등). 즉, 사용자가 직접 수신할 메일의 내용을 설정해준다. 그리고 후에 생성할 updatePassword 메서드의 파라미터로 임시비밀번호와 폼에서 입력한 이메일 값을 넣어준다.
mailSend
: 위의 createMailAndChangePassword
메소드를 통해 리턴된 MailDto 를 바탕으로 이메일을 전송한다. javaMailSender.send(message); 가 이 것을 담당하고 있다.
updatePassword
: 실제로 비밀번호를 변경하는 메서드이다.
한 가지 주의할 점은 member Entity에 새롭게 생성된 임시 password로 password를 변경하고자 할 때, BCryptPasswordEncoder
클래스로 임시 비밀번호를 암호화해야한다는 것이다. 그렇지 않다면 다음과 같이 Encode password does not look like Bcrypt 경고가 뜬다.
암호화를 하지 않았다면 변경된 비밀번호를 입력했음에도 불구하고 로그인이 안되는 현상을 볼 수 있다. 스프링 시큐리티가 암호화 처리가 안된 비밀번호는 아예 들여보내지 않기 때문... (보안 하나는 확실하다👍)
비밀번호를 전송하는 기능은 모두 구현했으니, 이메일 찾기 기능을 구현하자.
MemberController.java
// 회원 아이디 찾기
@RequestMapping(value = "/findId", method = RequestMethod.POST)
@ResponseBody
public String findId(@RequestParam("memberEmail") String memberEmail) {
String email = String.valueOf(memberRepository.findByEmail(memberEmail));
System.out.println("회원 이메일 = " + email);
if(email == null) {
return null;
} else {
return email;
}
}
사용자가 폼에 이메일을 입력했을 때, 존재하는 이메일이라면 email을 전송하고 존재하지 않는 이메일이라면 null을 전송한다. 이 것은 Ajax 요청을 통해 결과값으로 받아 처리할 것이다.
이제부터는 View의 영역이다.
<th:block layout:fragment="script">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script type="text/javascript">
$(document).on('click', '#checkEmail', function() {
const email = $("#email").val();
if (email == "" || email == null || email == undefined) {
alert("이메일을 입력해 주세요!")
return false;
}
const sendEmail = document.forms["sendEmail"];
$.ajax({
url : "/members/findId",
type : "post",
data : { 'memberEmail': email },
dataType : "text",
success : function(result) {
console.log(result)
if (result === null) {
alert('가입되지 않은 이메일입니다!');
} else {
alert('임시비밀번호를 전송 했습니다.');
sendEmail.submit();
}
},
error : function(xhr) {
alert("에러코드 = " + xhr.status);
}
});
});
</script>
</th:block>
<div id="container" layout:fragment="content">
<body>
<form role="form" class="text-start" method="post" name="sendEmail" action="/members/sendEmail">
<br></br>
<h1 class="title">비밀번호 찾기</h1>
<div class="login-form">
<div class="form-group">
<input type="email" id="email" name="memberEmail" class="form-control" placeholder="회원가입시 입력했던 이메일을 입력하세요." style="width:400px; margin-left:50px; margin-top:40px;" required>
<p style="margin-left:80px; margin-top:20px;">입력한 이메일로 임시 비밀번호가 전송됩니다!</p>
</div>
<p th:if="${loginErrorMsg}" class="error" th:text="${loginErrorMsg}"></p>
<div class="button_container">
<button type="button" id="checkEmail" class="button" style="margin-top:20px"> 비밀번호 발송 </button>
<br>
</div>
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
</div>
</form>
</body>
</div>
</html>
이번에도 오류는 정말 많았다... 그럼에도 차근차근 하나씩 해결해보았다..
왠 갑자기 생뚱맞은 구문 오류가 떴다. 무슨 오류인지 찾아보니...Mail send 즉, 메일을 보내는 과정에서 발생하였고, 그 내용은 Gmail SMTP 서버에 보내는 서버의 내용 중 어떠한 것을 읽지 못한다는 것이다. 문제 원인은 내 윈도우 계정이 한글이라는 것에 있었다. (더 어이없는 오류도 많으니 이정도는...😂)
https://javanitto.tistory.com/32 이 블로그에 나와있는 대로 내 계정 이름을 영어로 바꾸고 해결했다. 또 한번 감사합니다 (꾸벅)
Ajax에 error code를 찍게 설정했었는데, 저렇게 401이 떴다. 401 code에 나름 전문가가 되어있었기 때문에 바로 알았다. 권한 문제라는 것을...
SecurityConfig 에 바로 달려가보았다.
http.authorizeRequests()
.mvcMatchers("/css/**", "/js/**", "/img/**").permitAll()
.mvcMatchers("/", "/members/**", "/item/**", "/images/**", "/mail/**").permitAll()
.mvcMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
.and()
.csrf().ignoringAntMatchers("/mail/**")
/members/ 와 관련된 모든 경로는 인증 없이 접근할 수 있도록 설정해두었었는데... 뭐가 문제일까? 라는 생각을 하고 있을 때, csrf 문제라는 생각이 불현듯 지나갔다.
.csrf().ignoringAntMatchers("/mail/**")
.ignoringAntMatchers("/members/findId")
마찬가지로 csrf 예외 처리 설정을 함으로써 해결!
aa@aaa 라고 막 입력한 이메일에도 임시 비밀번호를 전송하는 모습이다..
원래 같으면 입력된 이메일을 MemberController
의 findId 메서드에서 memberRepository
이메일과 대조하고 null값 (즉, Repository에 찾는 이메일이 없음)이면 Ajax 리턴 값으로 "가입되지 않은 이메일입니다" 라는 알림이 떠야하는데 알림이 뜨지 않았고, 무조건 발송을 시켰다.
console.log(result) 를 통해 log를 찍어보면 Controller 로부터 null이라는 data값은 잘 들어오는 것 같은데 왜 if 분기에서 캐치를 못할까...
그렇게 삽질을 하고 또 하는 와중에...
혹시 null을 문자로 받는 것은 아닐까하여 다음과 같이 result === 'null'
로 바꾸어보니..
$.ajax({
url : "/members/findId",
type : "post",
data : { 'memberEmail': email },
dataType : "text",
success : function(result) {
console.log(result)
if (result === 'null') {
alert('가입되지 않은 이메일입니다!');
} else {
alert('임시비밀번호를 전송 했습니다.');
sendEmail.submit();
}
}
아주 잘되었다...😅 Javascript랑은 당분간 좀 멀리 떨어져 지낼 생각이다ㅎㅎ...
로그인 화면에서 밑의 링크를 타고 들어가면...
위처럼 비밀번호 찾는 폼을 마주할 수 있고, 회원가입을 했다는 가정하에 여기서 실제 회원가입을 통해 DB에 저장된 이메일을 입력해보자.
입력한 이메일이 DB에 있었기 때문에 임시 비밀번호가 전송되었다. 이제 메일함에 와있는 메일을 확인하자.
메일이 잘 도착했고, 이를 로그인 창에 입력했을 때, 로그인이 성공하는 것을 볼 수 있었다.