국비교육(22-23)

52일차(2)/Spring(11) : Interceptor 추가, 비밀번호 암호화 및 수정, 회원 삭제 기능 구현

서리/Seori 2022. 12. 20. 23:30

52일차(2)/Spring(11) : Interceptor 추가, 비밀번호 암호화 및 수정, 회원 삭제 기능 구현

 

 

UsersController (전체)

package com.sy.spring04.users.controller;

import java.net.URLEncoder;

import javax.servlet.http.HttpSession;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.ModelAndView;

import com.sy.spring04.users.dto.UsersDto;
import com.sy.spring04.users.service.UsersService;

@Controller
public class UsersController {

	@Autowired
	private UsersService service;
	
	/*
	 * GET 방식 /users/signup_form 요청을 처리할 메소드
	 * - 요청방식이 다르면 실행되지 않는다.
	 */
	@RequestMapping(method = RequestMethod.GET, value = "/users/signup_form")
	public String signupform() {
		
		return "users/signup_form";
	}
	//회원가입 요청 처리
	@RequestMapping(method = RequestMethod.POST, value = "/users/signup")
	public ModelAndView signup(ModelAndView mView, UsersDto dto) {
		service.addUser(dto);
		mView.setViewName("users/signup");		
		return mView;
	}
	//로그인 폼 요청 처리
	@RequestMapping(method = RequestMethod.GET, value="/users/loginform")
	public String loginForm() {
		
		return "users/loginform";
	}	
	//로그인 요청 처리
	@RequestMapping("/users/login")
	public ModelAndView login(ModelAndView mView, UsersDto dto, String url, HttpSession session) {
		/*
		 * 서비스에서 비즈니스 로직을 처리할 때 필요로 하는 객체를 컨트롤러에서 직접 전달해주어야 한다.
		 * 주로, HttpServletRequest, HttpServletResponse, HttpSession, ModelAndView
		 * 등등의 객체이다.
		 */
		service.loginProcess(dto, session);
		
		//로그인 후에 가야할 목적지 정보를 
		String encodedUrl=URLEncoder.encode(url);
		mView.addObject("url", url);
		mView.addObject("encodedUrl", encodedUrl);
		mView.setViewName("users/login");
		return mView;
	}
	
	//로그아웃 요청 처리
	@RequestMapping("/users/logout")
	public String logout(HttpSession session) {		
		//세션에서 id라는 키값으로 저장된 값 삭제
		session.removeAttribute("id");
		return "users/logout";
	}
	//개인정보 보기 요청 처리
	@RequestMapping("/users/info")
	public ModelAndView info(HttpSession session, ModelAndView mView) {
		
		service.getInfo(session, mView);
		
		mView.setViewName("users/info");
		return mView;		
	}	
	//비밀번호 수정 폼 요청 처리
	@RequestMapping("/users/pwd_updateform")
	public String pwdUpdateForm() {
		
		return "users/pwd_updateform";
	}
	
	@RequestMapping("users/pwd_update")
	public ModelAndView pwdUpdate(UsersDto dto, ModelAndView mView, HttpSession session) {
		//서비스에 필요한 객체의 참조값을 전달해서 비밀번호 수정 로직을 처리한다.
		service.updateUserPwd(session, dto, mView);
		//view page로 forward 이동해서 작업결과를 응답한다.
		mView.setViewName("users/pwd_update");
		return mView;
	}	
	//회원 탈퇴 요청 처리
	@RequestMapping("/users/delete")
	public ModelAndView delete(HttpSession session, ModelAndView mView) {
		
		service.deleteUser(session, mView);
		
		mView.setViewName("users/delete");
		return mView;
	}

 

UsersServiceImpl (전체)

package com.sy.spring04.users.service;

import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCrypt;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.ModelAndView;

import com.sy.spring04.users.dao.UsersDao;
import com.sy.spring04.users.dto.UsersDto;

@Service
public class UsersServiceImpl implements UsersService{

	@Autowired
	private UsersDao dao;
	
	@Override
	public Map<String, Object> isExistId(String inputId) {
		// TODO Auto-generated method stub
		return null;
	}
	
	//회원 한명의 정보를 추가하는 메소드
	@Override
	public void addUser(UsersDto dto) {
		//가입시 입력한 비밀번호를 읽어와서
		String pwd=dto.getPwd();
		//암호화한 후에
		BCryptPasswordEncoder encoder=new BCryptPasswordEncoder();
		String encodedPwd=encoder.encode(pwd);
		//dto에 다시 넣어준다.
		dto.setPwd(encodedPwd);
		//암호화된 비밀번호가 들어있는 dto를 dao에 전달해서 새로운 회원정보를 추가한다.
		dao.insert(dto);		
	}
	//로그인 처리를 하는 메소드
	@Override
	public void loginProcess(UsersDto dto, HttpSession session) {
		//입력한 정보가 맞는지 여부
		boolean isValid=false;
		//아이디를 이용해서 회원정보를 얻어온다.
		UsersDto resultDto=dao.getData(dto.getId());
		//만일 select된 회원정보가 존재하고 (아이디가 없을경우 null이 리턴된다.)
		if(resultDto!=null) {
			//Bcrypt 클래스의 static 메소드를 이용해서 입력한 비밀번호와 암호화해서 저장된 비밀번호의 일치 여부를 알아내야 한다.
			isValid=BCrypt.checkpw(dto.getPwd(), resultDto.getPwd());
		}
		//만일 유효한 정보이면
		if(isValid) {
			//로그인 처리를 한다.
			session.setAttribute("id", resultDto.getId());
		}
		
	}
	//정보를 가져오는 메소드
	@Override
	public void getInfo(HttpSession session, ModelAndView mView) {
		//로그인된 아이디를 읽어온다.
		String id=(String)session.getAttribute("id");
		//DB에서 회원정보를 얻어와서
		UsersDto dto=dao.getData(id);
		//ModelAndView 객체에 담아준다.
		mView.addObject("dto", dto);		
	}

	@Override
	public void updateUserPwd(HttpSession session, UsersDto dto, ModelAndView mView) {
		 //세션 영역에서 로그인된 아이디 읽어오기
	      String id=(String)session.getAttribute("id");
	      //DB 에 저장된 회원정보 얻어오기
	      UsersDto resultDto=dao.getData(id);
	      //DB 에 저장된 암호화된 비밀 번호
	      String encodedPwd=resultDto.getPwd();
	      //클라이언트가 입력한 비밀번호
	      String inputPwd=dto.getPwd();
	      //두 비밀번호가 일치하는지 확인
	      boolean isValid=BCrypt.checkpw(inputPwd, encodedPwd);
	      //만일 일치 한다면
	      if(isValid) {
	         //새로운 비밀번호를 암호화 한다.
	         BCryptPasswordEncoder encoder=new BCryptPasswordEncoder();
	         String encodedNewPwd=encoder.encode(dto.getNewPwd());
	         //암호화된 비밀번호를 dto 에 다시 넣어준다.
	         dto.setNewPwd(encodedNewPwd);
	         //dto 에 로그인된 아이디도 넣어준다.
	         dto.setId(id);
	         //dao 를 이용해서 DB 에 수정 반영한다.
	         dao.updatePwd(dto);
	         //로그아웃 처리
	         session.removeAttribute("id");
	      }
	      //작업의 성공여부를 ModelAndView 객체에 담아 놓는다(결국 HttpServletRequest 에 담긴다)
	      mView.addObject("isSuccess", isValid);
	      //로그인된 아이디도 담아준다.
	      mView.addObject("id", id);
		
	}

	@Override
	public Map<String, Object> saveProfileImage(HttpServletRequest request, MultipartFile mFile) {
		// TODO Auto-generated method stub
		return null;
	}

	@Override
	public void updateUser(UsersDto dto, HttpSession session) {
		// TODO Auto-generated method stub
		
	}

	@Override
	public void deleteUser(HttpSession session, ModelAndView mView) {
		//로그인된 아이디를 얻어와서
		String id=(String)session.getAttribute("id");
		//해당 정보를 DB에서 삭제하고
		dao.delete(id);
		//로그아웃 처리도 한다.
		session.removeAttribute("id");
		//ModelAndView 객체에 탈퇴한 회원의 아이디를 담아준다.
		mView.addObject("id", id);		
	}

}

 


 

- info(개인정보 보기) 페이지는 로그인한 사람만 들어갈 수 있도록 하기.

- Loginfilter대신 인터셉터 사용!

 

- com.sy.spring04.interceptor패키지 생성

LoginInterceptor (이전에 03에서 만든 LoginIntercepter 가져오기)

package com.sy.spring04.interceptor;

import java.net.URLEncoder;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

//로그인된 사용자인지 검사할 인터셉터
public class LoginInterceptor implements HandlerInterceptor {

	//Controller 메소드 수행 직전에 로그인된 사용자인지 검증을 해서
	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {
		//세션 객체의 참조값을 얻어와서
		HttpSession session=request.getSession();
		String id=(String)session.getAttribute("id");
		//만일 로그인을 하지 않았다면
		if(id==null) {
			//로그인 페이지로 리다일렉트 이동시키고 false를 리턴한다.
			
			//원래 가려던 url 정보 읽어오기
	         String url=request.getRequestURI();
	         //GET 방식 전송 파라미터를 query 문자열로 읽어오기 ( a=xxx&b=xxx&c=xxx )
	         String query=request.getQueryString();
	         //특수 문자는 인코딩을 해야한다.
	         String encodedUrl=null;
	         if(query==null) {//전송 파라미터가 없다면 
	            encodedUrl=URLEncoder.encode(url);
	         }else {
	            // 원래 목적지가 /test/xxx.jsp 라고 가정하면 아래와 같은 형식의 문자열을 만든다.
	            // "/test/xxx.jsp?a=xxx&b=xxx ..."
	            encodedUrl=URLEncoder.encode(url+"?"+query);
	         }
	         
	         //3. 로그인을 하지 않았다면 /users/loginform 페이지로 리다일렉트 이동 시킨다. (HttpServletResponse)
	         String cPath=request.getContextPath();
	         response.sendRedirect(cPath+"/users/loginform?url="+encodedUrl);
	         return false;
	      }

		//로그인을 했다면 흐름을 이어간다.
		return true;
	}

	@Override
	public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
			ModelAndView modelAndView) throws Exception {
		// TODO Auto-generated method stub
		
	}

	@Override
	public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
			throws Exception {
		// TODO Auto-generated method stub
		
	}

}

 

- 서버가 클라이언트를 구분하는 방법은? 클라이어트별로 고유한 session영역이 있다.

- 로그인 처리란 그 고유한 session영역에 클라이언트를 식별할 수 있는 값을 담아놓는 것이다.

 

- 인터셉터는 정의한다고 해서 동작하는 것은 아니다.

- bean으로 만들려면? 

→ @component 어노테이션을 붙여도 되고, servlet-context.xml 에 작성해줄 수도 있다.

 

- 이렇게 작성해주기. bean의 class는 ctrl+space로 자동완성시킬 수 있다.

 

- 이렇게 4개 페이지만 exclude(제외) 시키고, 나머지 모든 요청은 interceptor가 동작하도록 한다.

 

- 이제 로그아웃 상태에서 info 요청을 하면 자동으로 로그인 폼으로 이동한다.

- interceptor가 정상적으로 작동하는 것!

 

 

- 주소창을 보면 url이라는 파라미터명으로 원래 가야할 목적지 정보를 담고 있는 것을 볼 수 있다.(인코딩을 한 상태)

 


 

- 현재 quantum DB에서 테이블을 확인해 보면 이런데,

 원래는 관리자도 DB에서 사용자의 비밀번호를 볼 수 있으면 안된다.

→ 비밀번호를 암호화해서 저장해야 한다!

 

- pom.xml에 필요한 라이브러리는 이미 추가되어 있다.(원래는 추가해주어야 한다)

 

 

- 회원가입시에 비밀번호를 저장할 때 아예 암호화를 해야한다.

- DELETE FROM users 로 일단 현재 가입된 가입자 정보를 모두 삭제하기!

 

- service로 가기

//회원 한명의 정보를 추가하는 메소드
@Override
public void addUser(UsersDto dto) {
    //가입시 입력한 비밀번호를 읽어와서
    String pwd=dto.getPwd();
    //암호화한 후에
    BCryptPasswordEncoder encoder=new BCryptPasswordEncoder();
    String encodedPwd=encoder.encode(pwd);
    //dto에 다시 넣어준다.
    dto.setPwd(encodedPwd);
    //암호화된 비밀번호가 들어있는 dto를 dao에 전달해서 새로운 회원정보를 추가한다.
    dao.insert(dto);		
}

 

- 회원 한명의 정보를 추가하는 메소드(addUser)에서

그냥 DB에 추가하는 것이 아니고 암호화 과정을 거친다!

 

- 비밀번호 암호화에는 BCrypt password Encoder 객체를 사용한다.

 

- 메소드가 CharSequence 타입을 받는다. CharSequence 타입은 String이라고 생각하면 된다.

- 즉 문자열을 넣어주면 암호화된 문자열을 리턴해주는 메소드이다.

- 이 리턴된 값을 DB에 저장해주면 된다.

 

- String을 Serializable, CharSequence, Comparable 타입으로도 받을 수 있다.

- 모두 String이 구현한 인터페이스인 것!

 

- 이렇게 메소드를 저장해주면 다른 값으로 저장된다.

 

 

- 그러면 로그인 메소드도 수정해줘야 한다.

- 현재 상태로는 입력한 값이 DB의 변환된(암호화된) 글자와 같은지만을 비교하므로 로그인이 되지 않는다.

 

loginProcess (수정)

//로그인 처리를 하는 메소드
@Override
public void loginProcess(UsersDto dto, HttpSession session) {
    //입력한 정보가 맞는지 여부
    boolean isValid=false;
    //아이디를 이용해서 회원정보를 얻어온다.
    UsersDto resultDto=dao.getData(dto.getId());
    //만일 select된 회원정보가 존재하고 (아이디가 없을경우 null이 리턴된다.)
    if(resultDto!=null) {
        //Bcrypt 클래스의 static 메소드를 이용해서 입력한 비밀번호와 암호화해서 저장된 비밀번호의 일치 여부를 알아내야 한다.
        isValid=BCrypt.checkpw(dto.getPwd(), resultDto.getPwd());
    }
    //만일 유효한 정보이면
    if(isValid) {
        //로그인 처리를 한다.
        session.setAttribute("id", resultDto.getId());
    }
}

 

- 원본 pwd 문자열이 무엇인지는 알 수 없지만 입력한 값과의 일치 여부는 알아낼 수 있다.

 

- BCrypt의 checkpw() 메소드를 사용한다!

- 일반 텍스트, hashed된 텍스트를 넣으면 boolean 값을 리턴해준다.

 

- 이렇게 수정할 수 있다.

 

- BCrypt 객체를 사용하려면 security 패키지가 사용 가능해야 한다.(그래야 import 가능)

- 이 패키지를 사용가능하게 하기 위해 위의 pom.xml에 명시해놓은 것!

 


 

비밀번호 수정 updatepwd

- 컨트롤러 추가

//비밀번호 수정 폼 요청 처리
@RequestMapping("/users/pwd_updateform")
public String pwdUpdateForm() {

    return "users/pwd_updateform";
}

 

pwd_updateform.jsp 추가

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>    
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>/views/users/pwd_updateform.jsp</title>
</head>
<body>
<div class="container">
   <h1>비밀 번호 수정 폼</h1>
   <form action="${pageContext.request.contextPath}/users/pwd_update" method="post" id="myForm">
      <div>
         <label for="pwd">기존 비밀 번호</label>
         <input type="password" name="pwd" id="pwd"/>
      </div>
      <div>
         <label for="newPwd">새 비밀번호</label>
         <input type="password" name="newPwd" id="newPwd"/>
      </div>
      <div>
         <label for="newPwd2">새 비밀번호 확인</label>
         <input type="password" id="newPwd2"/>
      </div>
      <button type="submit">수정하기</button>
      <button type="reset">리셋</button>
   </form>
</div>
<script>
   //폼에 submit 이벤트가 일어났을때 실행할 함수를 등록하고 
   document.querySelector("#myForm").addEventListener("submit", function(e){
      let pwd1=document.querySelector("#newPwd").value;
      let pwd2=document.querySelector("#newPwd2").value;
      //새 비밀번호와 비밀번호 확인이 일치하지 않으면 폼 전송을 막는다.
      if(pwd1 != pwd2){
         alert("비밀번호를 확인 하세요!");
         e.preventDefault();//폼 전송 막기 
      }
   });
</script>
</body>
</html>

 

- 비밀번호 두개를 똑같이 작성하지 않으면 javascrpit로 전송을 막는다.

 

 

- pwd_update 컨트롤러 추가

@RequestMapping("users/pwd_update")
public ModelAndView pwdUpdate(UsersDto dto, ModelAndView mView, HttpSession session) {
    //서비스에 필요한 객체의 참조값을 전달해서 비밀번호 수정 로직을 처리한다.
    service.updateUserPwd(session, dto, mView);
    //view page로 forward 이동해서 작업결과를 응답한다.
    mView.setViewName("users/pwd_update");
    return mView;
}

 

- 로그인된 아이디도 필요하므로 인자로 session도 받아야 한다.

 

- 안에서 메소드를 호출하면 이렇게 전달된다.

 

 

- 서비스 updateUserPwd 메소드

@Override
public void updateUserPwd(HttpSession session, UsersDto dto, ModelAndView mView) {
     //세션 영역에서 로그인된 아이디 읽어오기
      String id=(String)session.getAttribute("id");
      //DB 에 저장된 회원정보 얻어오기
      UsersDto resultDto=dao.getData(id);
      //DB 에 저장된 암호화된 비밀 번호
      String encodedPwd=resultDto.getPwd();
      //클라이언트가 입력한 비밀번호
      String inputPwd=dto.getPwd();
      //두 비밀번호가 일치하는지 확인
      boolean isValid=BCrypt.checkpw(inputPwd, encodedPwd);
      //만일 일치 한다면
      if(isValid) {
         //새로운 비밀번호를 암호화 한다.
         BCryptPasswordEncoder encoder=new BCryptPasswordEncoder();
         String encodedNewPwd=encoder.encode(dto.getNewPwd());
         //암호화된 비밀번호를 dto 에 다시 넣어준다.
         dto.setNewPwd(encodedNewPwd);
         //dto 에 로그인된 아이디도 넣어준다.
         dto.setId(id);
         //dao 를 이용해서 DB 에 수정 반영한다.
         dao.updatePwd(dto);
         //로그아웃 처리
         session.removeAttribute("id");
      }
      //작업의 성공여부를 ModelAndView 객체에 담아 놓는다(결국 HttpServletRequest 에 담긴다)
      mView.addObject("isSuccess", isValid);
      //로그인된 아이디도 담아준다.
      mView.addObject("id", id);

}

 

- 해야할 것이 많다.

1) 암호화된 비밀번호를 가져와서 입력한 값과 같은지 확인(기존 비밀번호가 맞지 않으면 처리해주면 안된다.)

2) 새 비밀번호 암호화해서 dto에 넣어서 DB에 수정 반영 + 로그아웃 처리

3) 응답

 

1) 기존 비밀번호 입력값이 DB에 저장된 비번과 일치하는지

 

2) 새 비밀번호를 암호화해서 수정반영

 

 

3) 응답을 위해 성공여부와 현재 로그인된 id를 담아준다.

- ModelAndview에 담으면 session이 아니라 request 영역에 담긴다.

 

 

pwd_update.jsp 생성

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>    
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>/views/users/pwd_update.jsp</title>
</head>
<body>
<div class="container">
   <c:choose>
      <c:when test="${isSuccess }">
         <p>
            <strong>${id }</strong> 님 비밀번호를 수정하고 로그아웃되었습니다.
            <a href="${pageContext.request.contextPath}/users/loginform">로그인하러 가기</a>
         </p>
      </c:when>
      <c:otherwise>
         <p>
            이전 비밀번호가 일치하지 않습니다.
            <a href="${pageContext.request.contextPath}/users/pwd_updateform">다시 시도</a>
         </p>
      </c:otherwise>
   </c:choose>
</div>
</body>
</html>

 

<c:when test="${isSuccess }">

- 위 코드를 사용해서 성공여부를 읽어와서 성공/실패 여부에 따라 다른 응답을 해준다.

 

- 새 비밀번호/새 비밀번호 확인 이 일치하지 않을 경우!

 

 

- 비밀번호 변경 완료!

 


 

- delete 요청 처리

 

- controller

//회원 탈퇴 요청 처리
@RequestMapping("/users/delete")
public ModelAndView delete(HttpSession session, ModelAndView mView) {

    service.deleteUser(session, mView);

    mView.setViewName("users/delete");
    return mView;
}

 

서비스 만들기(deleteUser)

@Override
public void deleteUser(HttpSession session, ModelAndView mView) {
    //로그인된 아이디를 얻어와서
    String id=(String)session.getAttribute("id");
    //해당 정보를 DB에서 삭제하고
    dao.delete(id);
    //로그아웃 처리도 한다.
    session.removeAttribute("id");
    //ModelAndView 객체에 탈퇴한 회원의 아이디를 담아준다.
    mView.addObject("id", id);		
}

 

- 이미 지웠기 때문에 ModelAndView 객체에 탈퇴한 아이디를 다시 담아주는 작업이 필요한 것

 

 

delete.jsp 

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>/views/users/delete.jsp</title>
</head>
<body>
	<div class="container">
		<h1>알림</h1>
		<p>
			<strong>${requestScope.id}</strong> 님 탈퇴 처리 되었습니다.
			<a href="${pageContext.request.contextPath}/">인덱스로 가기</a>
		</p>
	</div>
</body>
</html>

 

 

- 위에서 ModelAndView 객체에 담은 것을 여기서 활용!

 

 

- 회원 삭제에 대한 응답!

 


 

참고) ChatGPT  (chat.openai.com/chat)

- 개발 관련된 질문으로 인공지능이 채팅을해주는 open ai 이다.(개발관련)

- 알아서 검색해서 추천 코드도 주고 대화도 해준다.

 

- 인간인지 ai인지 구분하기 어렵다고 한다.

- 내 코드를 넣어주면 리뷰도 해주고, 질문하면 답변을 주기도 한다.

 

- 이런식으로 질문하면

 

- 이렇게 답변해준다.

 

 

- 이런식으로 코드 샘플을 주기도 한다.

- AI가 이 대화를 generate 하는것!

 

- 위의 updateUserPwd 를 넣어보니 이렇게 코드 리뷰를 해주었다... 공부하는 데에 참고!