개발자꿈나무
Fureverhomes 프로젝트 - 로그인 & 비밀번호 재설정 본문
1. 회원 로그인
MemberService - 로그인
이번에는 세션에 로그인 정보를 담아서 관리를 할 생각이다. LoginDTO를 하나 만들어서 로그인 기능을 구현할지 고민하다가 그냥 Map 타입으로 값을 받아와서 사용을 할 예정이다.
//로그인
public Long singin(final Map<String,String> mapParam) {
String email = mapParam.get("email");
String password = mapParam.get("password");
Member member = memberRepository.findByEmail(email);
String memberPwd = (member == null) ? "" : member.getPassword();
if(member == null || (!passwordEncoder.matches(password, memberPwd))){
return null;
} return member.getId();
}
멤버 안에 있는 비밀번호는 암호화가 되어 있는 값이기 때문에 passwordEncoder.matches()를 이용해서 입력받은 값과 들어있는 값이 같은 값인지 비교해준다. matches의 파라미터에는 (암호화되지 않은 값, 암호화가 된 값)을 넣어줘야 한다.
MemberController - 로그인 / 로그아웃
//로그인
@PostMapping("/signin.post")
public ResponseEntity<Object> signin(@RequestBody Map<String, String> params, HttpServletRequest request) {
//1. 회원 정보 조회
Long loginMember = memberService.singin(params);
//2. 세션에 회원 정보 저장 & 만료 시간 설정
if (loginMember != null) {
HttpSession session = request.getSession();
session.setAttribute("longinMember", loginMember);
session.setMaxInactiveInterval(60 * 30); //30분
return ResponseEntity.ok().build();
}
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
//로그아웃
@PostMapping("/signout.post")
public ResponseEntity<Object> signout(HttpServletRequest request) {
HttpSession session = request.getSession();
if (session != null) {
session.invalidate();
}
return ResponseEntity.ok().build();
}
세션 관련 과정은 컨트롤러에서 진행하는게 좋다고 한다. 일치하는 회원 정보가 존재할때할 때 session에 loginMember를 저장해둔다. Member의 식별자인 id 값을 넣어놓고 관리하도록 만들었다. 로그아웃은 1. 로그아웃 버튼 누르기 2. 세션 시간 만료되기 두 가지 방법이 있다.
LoginCheckInterceptor
로그인 되어있는 사용자인지 아닌지를 확인하기 위해 인터셉터를 하나 작성해준다. 서버에 요청이 들어오면 제일 먼저 실행되어야 하는 기능으로 preHandle을 오버라이딩 해준다.
//세션에 로그인 정보가 있는지를 확인하기 위한 인터셉터
public class LoginCheckInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//1.세션에서 회원 정보 조회
HttpSession session = request.getSession();
Long member = (Long) session.getAttribute("longinMember");
//2. 만료시 로그인 페이지로 이동
if (member == null) {
response.sendRedirect("/fureverhomes/signin");
return false;
}
return HandlerInterceptor.super.preHandle(request, response, handler);
}
}
WebMvcConfig
스프링이 인터셉터 클래스를 인식할 수 있도록 Config 클래스에 등록해준다.
addInterceptor()에 등록하고자 하는 LoginCheckInterceptor클래스를 적어주고 addPathPatterns와 excludePathPatterns를 적어준다. addPathPatterns 인터셉터를 실행시킬 패턴을 명시해주면 되고, excludePathPatterns는 제외할 패턴을 적어주면 된다.
잘못하면 css, js 등 static에 들어있는 모든 파일들도 인터셉터로 인해 접근이 불가능하므로 주의해서 적어준다.
또한 메인페이지, 로그인, 회원가입과 관련된 활동들 역시도 인터셉터가 실행되지 않아야 하므로 excludePathPatterns에 적어준다. 그렇지 않으면 계속 로그인 페이지로 이동하게 될수도 있다.
@Configuration
public class WebMvcConfiig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginCheckInterceptor())
.addPathPatterns("/fureverhomes/**")
.excludePathPatterns("/fureverhomes/main", "/fureverhomes/signin/**", "/fureverhomes/signup/**" ,
"/fureverhomes/signin.**","/fureverhomes/signup.**");
}
}
로그인.js
$(document).ready(function() {
$("#signin").click(function () {
let email = $("#email").val();
let password = $("#password").val();
if (!email) {
alert("이메일을 입력해주세요.");
email.focus();
} else if (!password) {
alert("비밀번호를 입력해주세요.");
password.focus();
}
$.ajax({
type: "POST",
url: "/fureverhomes/signin.post",
contentType: 'application/json',
data: JSON.stringify({
email : email,
password : password,
}),
async: false,
success: function (data, status) {
alert("성공!")
location.href = "/fureverhomes/animal";
},
error: function (data, textStatus) {
alert("로그인 정보가 맞지 않습니다.")
location.href = "/fureverhomes/signin";
}
});
});
});
2. 비밀번호 재설정
비밀번호 재설정의 경우 사용자가 이메일을 적으면 등록된 회원인지 여부를 파악한 후에 비밀번호 재설정 페이지 정보를 담은 링크를 전송해준다. 한번 비밀번호를 재설정이 이루어진 다음에는 다시 링크에 들어가 변경을 해도 변경이 되지 않고, 5분이 지나면 링크가 만료되어 접근할 수 없도록 만들어주었다.
MemberService - 비밀번호 재설정
private Map<String, Long> linkExpirationMap = new ConcurrentHashMap<>();
//비밀번호 재설정 링크 전송
public Boolean sendLinkMail(final @RequestBody MailDTO mailDTO) throws MessagingException {
String email = mailDTO.getEmail();
Member member = memberRepository.findByEmail(email);
if (member != null && member.getEmail_auth() == 1) {
return sendMail(email);
} return null;
}
//링크 담은 메일 전송
private Boolean sendMail(final String email) throws MessagingException {
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
String linkId = UUID.randomUUID().toString();
String link = "http://localhost:8081/fureverhomes/signin/changePassword?linkId=" + linkId; //페이지 링크
long expirationTime = System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(5); // 만료 시간 설정
linkExpirationMap.put(linkId, expirationTime); // 링크별 만료 시간 저장
String mailContent = "<html><body>" +
"<p>아래 링크로 들어가 비밀번호를 재설정해주세요.</p>" +
"<br><a href='" + link + "'>5분이 지나면 링크가 만료됩니다.</a>" +
"</body></html>";
helper.setTo(email);
helper.setSubject("fureverhomes 비밀번호 재설정 안내");
helper.setText(mailContent, true);
System.out.println("[message] "+ message);
try {
mailSender.send(message);
System.out.println("이메일 전송 성공");
return true;
}catch(MailException e) {
System.out.println("이메일 전송 실패");
return false;
}
}
//링크 만료 시간 체크
public Long getLinkExpirationMap(final String linkId) {
return linkExpirationMap.get(linkId);
}
- sendLinkMail()
이메일 인증 시 메일을 보냈던 것처럼 mailDTO를 이용해서 메일을 보낼 것이다. member에 대한 정보가 존재하고 이메일 인증이 완료된 회원이라면 sendMail()을 실행한다. 만약 정보가 없거나 이메일 인증이 되지 않은 회원이라며 Null을 반환해서 클라이언트측에 이메일 전송 실패와 회원정보 없음 두 가지 응답을 보낼 예정이다.
- sendMail()
이번에는 메일에 링크 정보를 담아서 보낼거기 때문에 SimpleMailMessage가 아니라 MimeMessage를 이용할 거다. SimpleMailMessage는 단순히 텍스트 정보만을 보낼 수 있고, MimeMessage는 html, 파일 첨부나 이미지 등 멀티파트를 사용하여 전송할 수 있다. MimeMessageHelper의 두번째 파라미터값으로 true를 설정하면 멀티파트 메세지를 사용하겠다는 뜻이다. 직접 link주소와 html 태그를 이용해서 보내고자 하는 내용을 만들어주면 된다.
링크에 UUID를 함께 담아준 이유는 링크 만료시간을 파악하기 위해서이다. UUID별로 생성된 시간 + 5분의 정보를 담아 map에 넣어줬다.
링크 주소를 설정할때 http부터 전체를 설정해줘야한다. 웹 브라우저에서는 굳이 http://를 작성하지 않아도 알아서 이동을 해주지만 우리는 자바로 이메일링크를 생성해 보내는 것으로 주소를 처음부터 적어줘야 한다. (처음에 이거 때문에 약간 애를 먹었었다.)
MemberController - 비밀번호 재설정 링크 전송
//비밀번호 재설정 링크 전송
@PostMapping("/signin/sendLink.post")
public ResponseEntity<Object> sendResetPwdLink(@RequestBody MailDTO mailDTO, HttpServletRequest request) throws MessagingException {
if (memberService.sendLinkMail(mailDTO)) {
HttpSession session = request.getSession();
session.setAttribute("email", mailDTO.getEmail());
return ResponseEntity.ok().build();
} else if (!memberService.sendLinkMail(mailDTO)) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
} return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
처음엔 이메일 인증처럼 sessionStorage에 유저의 이메일 정보를 저장해서 처리하려고 했으나 이메일을 통해 링크에 접속하게 되면 새로운 창을 띄우기 때문에 sessionStorage에 담겨있던 이메일 정보가 사라지게 된다. 그래서 서버에서 Session에 이메일 정보를 담아두도록 변경하였다. 메일 전송이 무사히 되면 ok 응답코드를, 전송이 되지 않아 false를 반환받게 되면 BAD_REQUEST를 null을 반환받으면 UNAUTHORIZED 응답코드를 반환하도록 설정하였다.
비밀번호 링크 전송.js
$(document).ready(function () {
$("#sendResetPasswordLink").click(function (key, value) {
let email = $("#email").val();
$.ajax({
type: "POST",
url: "/fureverhomes/signin/sendLink.post",
contentType: 'application/json',
data: JSON.stringify({
email: email
}),
success: function (data) {
alert("이메일이 전송되었습니다. 메일을 확인해주세요.");
location.href = "/fureverhomes/signin";
},
error: function (data, status, xhr) {
if(xhr.status === 401) {
alert("존재하지 않는 회원정보입니다. 회원가입을 시도해주세요.");
location.href = "/fureverhomes/signup";
} else {
alert("이메일 전송에 실패했습니다. 이메일을 다시 확인해주세요.");
location.href = "/fureverhomes/main";
}
},
});
});
});
ajax error 응답코드에 따라 다르게 처리해주도록 해줬다.
3. 비밀번호 변경하기
MemberService - 비밀번호 변경
//비밀번호 변경
@Transactional
public Boolean changePassword(final Map<String, String> mapParam) {
String email = mapParam.get("email");
String newPassword = mapParam.get("password");
Member member = memberRepository.findByEmail(email);
if (member != null) {
member.updatePassword(passwordEncoder.encode(newPassword));
memberRepository.save(member);
return true;
} return false;
}
member 엔티티에 직접적으로 set하는걸 피하기 위해 updatePassword 메소드를 만들고 인코딩된 비밀번호 정보를 넘겨준 후 저장을 해줬다.
//비밀번호 변경하기
@PostMapping("/signin/change.post")
public ResponseEntity<Object> changePassword(@RequestBody Map<String,String> mapParam, HttpServletRequest request) {
HttpSession session = request.getSession();
String email = (String) session.getAttribute("email");
mapParam.put("email", email);
session.removeAttribute("email");
if (memberService.changePassword(mapParam)) {
return ResponseEntity.ok().build();
} return ResponseEntity.internalServerError().build();
}
session에 있는 이메일을 가져오고 mapParam에 이메일과 비밀번호 정보가 넘어가도록 해줬다. 비밀번호 재설정 기능들은 이메일 인증 기능과 비슷한 부분이 많아서 빠르게 진행할 수 있었다.
로그인 & 비밀번호 변경 테스트
로그인 창에서 이메일과 비밀번호를 잘못 적었을 경우와 제대로 값을 넣은 경우이다.
회원정보를 제대로 입력하면 로그인이 성공하고 사이트에 접속할 수 있게 된다. (아직 html 정리가 안되어있어서 모습이 이렇다..)
로그아웃 버튼을 클릭하게 되면 메인 페이지로 이동하고 로그아웃이 된 상태에서 현재와 같은 페이지에 입장하려고 해보겠다.
접근자체가 안되면 자동으로 로그인창으로 넘어가게 된다.
이번엔 비밀번호 찾기를 해보도록 하겠다. 비밀번호 찾기를 누르면 이메일 입력 창으로 넘어가고, 이메일을 입력하면 메일이 전송된다.
이메일이 무사히 도착한 모습이고, 해당링크로 들어가면 비밀번호 재설정 창이 뜬다. 주소창에는 UUID를 숨기고 간략하게 보여질 수 있도록 만들어주었다. 비밀번호를 재설정 해준 후에 다시 로그인을 하면 무사히 로그인에 성공한다.
만약 5분이 지난 링크에 접근하게 된다면 어떻게 될까? 어제 받았던 링크에 접근하도록 해보겠다.
그럼 이렇게 만료된 링크라는 오류 메세지가 뜨며, 나중에 오류 페이지와 연동해서 처리해주면 된다.
정리
드디어 회원과 관련된 기능들을 모두 마쳤다..!! 로그인과 비밀번호 재설정 기능은 예전 프로젝트에서 내가 맡아서 진행을 했던 부분이였기 때문에 비교적 수월하게 진행할 수 있었고, 이전에 짰던 로직들을 보면서 부족했던 부분이나 불필요했던 부분들을 한번 더 확인하고 좀 더 클린코드로 만들기 위해 생각할 수 있는 시간이였던 것 같다. 저번에는 Jwt를 이용해서 구현을 하는게 꽤나 애를 먹었었는데 이번에는 빠른 진행을 위해 세션으로 관리를 하도록 만들어뒀는데 또다른 기본적인 부분을 체크할 수 있었던 것 같다. 이제 본격적인 기능 구현들이 남았다..!! 더 험난할 것 같아서 걱정이 되지만 JPA는 확실히 어떻게 이용을 해야할지 조금씩 정리가 되어가는 느낌이다.
다만 지금 짜고있는 구성이 낮은 결합도와 높은 응집도를 이루고 있는가는 잘 모르겠어서 공부를 더 해야할 것 같다고 느낀다.
'기술블로그' 카테고리의 다른 글
FureverHomes 프로젝트 - 검색과 페이징 (0) | 2023.09.26 |
---|---|
FureverHomes 프로젝트 - 동물 정보 조회 (0) | 2023.09.23 |
FureverHomes 프로젝트 - 회원가입 2. 이메일 인증 (0) | 2023.09.21 |
FureverHomes 프로젝트 - 회원 가입 1. 회원 저장과 중복 확인 (0) | 2023.09.19 |
FureverHomes 프로젝트 - 1. 기본세팅과 JPA 설정 (2) | 2023.09.18 |