티스토리 뷰
KH 정보교육원 [ Java ]
KH 124일차 - Spring ( 인터셉터 - 세션 트레킹 : 로그인 / 로그아웃 / 인증 / 인가 / 자동 로그인 기능 ) (*********)
monimoni 2022. 8. 24. 15:41
[ 1. 인터셉터 - 세션 트레킹 ( 로그인 / 로그아웃 / 인증 / 인가 / 자동 로그인 처리 ) ] (******)
[ + DTO는 전의 게시물 LoginDTO를 그대로 사용합니다. ]
더보기
[ + 코드 보기 ]
package org.zerock.myapp.domain;
import lombok.Data;
@Data
public class LoginDTO {
private String userid;
private String userpw;
// + 참조타입의 기본값은 NULL이다.
// + 따라서 체크박스의 check 여부의 기본값은 false로 지정하는 것이 좋다.
private Boolean rememberMe = false;
} // end class
[ + SharedScopeKeys는 공유데이터에 바인딩할때 Key로 사용하기 위해서 만든 클래스입니다. ]
더보기
[ + 코드 보기 ]
package org.zerock.myapp.common;
public final class SharedScopeKeys {
public static final String LOGIN_KEY = "__LOGIN_KEY__";
public static final String RESULT = "__RESULT__";
public static final String EXCEPTION = "__EXCEPTION__";
public static final String USER_KEY = "__USER_KEY__";
public static final String REMEMBER_ME_KEY = "__REMEMBER_ME__";
} // end class
[ 1 - 1. DAO 인터페이스 생성 ]
더보기
[ + 코드 보기 ]
package org.zerock.myapp.persistence;
import org.zerock.myapp.domain.LoginDTO;
import org.zerock.myapp.domain.UserVO;
import org.zerock.myapp.exception.DAOException;
public interface UserDAO {
// 1. 로그인 창에서 입력한 아이디와 암호에 매칭되는 사용자의 정보 획득
public abstract UserVO selectUser(LoginDTO dto) throws DAOException;
} // end interface
[ 1 - 2. mapper에 sql문 등록 ]
더보기
[ + 코드 보기 ]
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.zerock.myapp.persistence.UserDAO">
<select
id="selectUser"
resultType="org.zerock.myapp.domain.UserVO">
<![CDATA[
SELECT *
FROM tbl_user
WHERE userid = #{userid} AND userpw = #{userpw}
]]>
</select>
<!-- ========================================================================================= -->
</mapper>
[ + 마이바티스 설정 파일에서 mapper 등록 ]
[ 1 - 3. DAO 구현 ]
더보기
[ + 코드 보기 ]
package org.zerock.myapp.persistence;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import org.zerock.myapp.domain.LoginDTO;
import org.zerock.myapp.domain.UserVO;
import org.zerock.myapp.exception.DAOException;
import lombok.NoArgsConstructor;
import lombok.extern.log4j.Log4j2;
@Log4j2
@NoArgsConstructor
@Repository
public class UserDAOImpl implements UserDAO {
@Autowired
private SqlSessionFactory sqlSessionFactory;
@Override
public UserVO selectUser(LoginDTO dto) throws DAOException {
log.trace("selectUser({}) invoked.", dto);
SqlSession sqlSession = this.sqlSessionFactory.openSession();
try ( sqlSession ){
String namespace = "org.zerock.myapp.persistence.UserDAO";
String sqlId = "selectUser";
String sql = namespace + "." + sqlId;
return sqlSession.<UserVO>selectOne(sql, dto);
} catch(Exception e) {
throw new DAOException(e);
} // try - with - resources
} // selectUser
} // end class
[ 1 - 4. Service 인터페이스 생성 ]
더보기
[ + 코드 보기 ]
package org.zerock.myapp.service;
import org.zerock.myapp.domain.LoginDTO;
import org.zerock.myapp.domain.UserVO;
import org.zerock.myapp.exception.DAOException;
import org.zerock.myapp.exception.ServiceException;
public interface UserService {
// =====================================================================
// 1. 로그인 창에서 입력한 아이디와 암호에 매칭되는 회원이 있는지 확인
// =====================================================================
// ReturnType을 Boolean이 아닌 UserVO로 하는 이유는,
// 이 객체를 바로 로그인 성공 정보로 Session Scope에 바인딩시키기 위해서이다.
public abstract UserVO login(LoginDTO dto) throws ServiceException;
} // end interface
[ 1 - 5. Service 구현 ]
더보기
[ + 코드 보기 ]
package org.zerock.myapp.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.zerock.myapp.domain.LoginDTO;
import org.zerock.myapp.domain.UserVO;
import org.zerock.myapp.exception.DAOException;
import org.zerock.myapp.exception.ServiceException;
import org.zerock.myapp.persistence.UserDAO;
import lombok.AllArgsConstructor;
import lombok.extern.log4j.Log4j2;
@Log4j2
@AllArgsConstructor
@Service
public class UserServiceImpl implements UserService {
// ===============================================================
@Autowired
private UserDAO userDAO;
// ===============================================================
// + 1. 로그인 처리
// ===============================================================
@Override
public UserVO login(LoginDTO dto) throws ServiceException {
log.trace("login({}) invoked.", dto);
try {
return this.userDAO.selectUser(dto);
} catch ( DAOException e ) {
throw new ServiceException(e);
} // try - catch
} // login
// ===============================================================
} // end class
[ 1 - 6. 컨트롤러 생성 ]
더보기
[ + 코드 보기 ]
package org.zerock.myapp.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import org.zerock.myapp.common.SharedScopeKeys;
import org.zerock.myapp.domain.LoginDTO;
import org.zerock.myapp.domain.UserVO;
import org.zerock.myapp.exception.ControllerException;
import org.zerock.myapp.service.UserService;
import lombok.NoArgsConstructor;
import lombok.extern.log4j.Log4j2;
@Log4j2
@NoArgsConstructor
@RequestMapping("/user/")
@Controller
public class LoginController {
@Autowired
private UserService service;
// ===============================================================================================================
// + 1. 로그인 처리
// ===============================================================================================================
@PostMapping("/loginPost")
// + @ModelAttribute 어노테이션을 통해 모델에 저장할 수 있다.
public String loginPost ( /* @ModelAttribute("loginDTO") */ LoginDTO dto, Model model, RedirectAttributes rttrs )
throws ControllerException {
log.trace("loginPost({}, {}) invoked.", dto, model);
// + loginPost(LoginDTO(userid=hohohoh, userpw=1234, rememberMe=true)) invoked.
// + view에서 post로 정보가 들어온다.
try {
UserVO vo = this.service.login(dto);
log.info("\t + vo : {}", vo);
if( vo != null ) { // 성공했을 때
model.addAttribute(SharedScopeKeys.LOGIN_KEY, vo);
return "user/loginPost";
// + redirect로 할 경우에는 Request Scope이 깨지기 때문에, model안의 vo가 나오지 못한다.
} else { // 실패했을 때
rttrs.addAttribute(SharedScopeKeys.RESULT, "Login Failed.");
return "redirect:/user/login";
} // if - else
} catch (Exception e) {
throw new ControllerException(e);
} // try - catch
} // loginPost
// ===============================================================================================================
// + 2. 로그 아웃 처리
// ===============================================================================================================
// + 로그 아웃 요청시에 수행되어야 할 일 : --> LogoutInterceptor가 수행
// + 1. Session Scope 자체를 파괴시켜야 한다. ( 즉, session.invalidate() 메소드 수행 )
// + 2. 현재 웹 브라우저에 할당된 이름( 세션 ID )도 삭제
// ===============================================================================================================
@GetMapping("/logout")
public void logout( RedirectAttributes rttrs ) throws ControllerException {
log.trace("logout() invoked.");
// + 로그아웃은 LogoutInterceptor를 설정하기 위한 용도로만 사용될 뿐, 진짜 호출되지는 않는다.
} // logout
// ===============================================================================================================
} // end class
[ 1 - 7. 인터셉터 생성 ]
[ + 로그인 / 자동 로그인 처리 인터셉터 ]
더보기
[ + 코드 보기 ]
package org.zerock.myapp.interceptor;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.stereotype.Component;
import org.springframework.ui.ModelMap;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import org.zerock.myapp.common.SharedScopeKeys;
import org.zerock.myapp.domain.UserVO;
import lombok.NoArgsConstructor;
import lombok.extern.log4j.Log4j2;
@Log4j2
@NoArgsConstructor
@Component
public class LoginInterceptor implements HandlerInterceptor {
// ===================================================================================================
// + 전제 :
// + 로그인에 성공하면 UserVO를 Session Scope에 올려 놓을 예정이다.
// ===================================================================================================
// + /user/loginPost로 접속 -> preHandle -> LoginController의 로그인 처리 -> postHandle
// + preHandle에서는 이전에 접속한
// ===================================================================================================
// ===================================================================================================
// + 1. preHandle : 로그인
// ===================================================================================================
// + Incoming Request가 Controller's Handler Method로 위임되기 직전에 가로 채는 부분
// + Session Scope에 저장되어 있는 모든 정보를 파괴 수행
// + (*주의*) 명시적으로 로그아웃 요청을 보내지 않는 이상, 세션 자체를 파괴해서는 안된다.
// ===================================================================================================
@Override
public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object handler) throws Exception {
log.info("========================================================");
log.trace("1. preHandle(req, res, handler) invoked.");
log.info("========================================================");
// ===================================================================
// 1. Session Scope에 접근할 수 있는 HttpSession 객체 획득
// ===================================================================
HttpSession session = req.getSession();
// ===================================================================
// 2. Session Scope에 UserVO 객체가 공유 되어 있으면 삭제 처리
// ===================================================================
UserVO vo = (UserVO) session.getAttribute(SharedScopeKeys.USER_KEY);
// + 현재 Session Scope에 UserVO가 있는지, 로그인 되어있는 상태인지 확인
if ( vo != null ) {
session.removeAttribute(SharedScopeKeys.USER_KEY);
log.info("\t + Remove UserVO : {}", vo);
} else {
log.info("\t + No UserVO found in Session Scope");
} // if : null이 아니라면, UserVO가 Session Scope에 올려진 상태이기에 삭제해버린다.
return true;
} // preHandle
// ===================================================================================================
// + 2. postHandle : 로그인 성공 증빙 + 자동 로그인 처리
// ===================================================================================================
// + 만약 컨트롤러에서 예외가 발생하면, 아래의 callback 메소드는 실행되지 않는다.
@Override
public void postHandle(HttpServletRequest req, HttpServletResponse res, Object handler,
ModelAndView modelAndView) throws Exception {
log.info("========================================================");
log.trace("2. postHandle(req, res, handler, {}) invoked.", modelAndView);
log.info("========================================================");
// ===================================================================
// 1. 로그인 성공 증빙 데이터 바인딩
// ===================================================================
// + 매개변수인 modelAndView에 Model 상자 안에 UserVO 객체가 있는지 확인해 보고 ( 즉, 로그인 결과 )
// + 만약 성공했다면, 이 UserVO 객체를 Session Scope에 있는 로그인 성공증빙으로 올려 놓기로 한다.
// ===================================================================
// 1 ) modelAndView 객체 안에 UserVO가 있는지 확인
// ===================================================================
ModelMap modelMap = modelAndView.getModelMap();
UserVO vo = (UserVO) modelMap.getAttribute(SharedScopeKeys.LOGIN_KEY);
// ===================================================================
// 2 ) UserVO가 있다면( 로그인 성공 시 ) Session Scope에 UserVO를 올려 놓는다. ( 로그인 성공 증빙 )
// ===================================================================
if ( vo != null ) { // + 성공했을 때
// + 현재 Session Scope에 접근하기 위해 HttpSession 객체 생성
HttpSession session = req.getSession();
// + Session Scope에 공유 데이터로 로그인 성공 증빙 데이터를 바인딩
session.setAttribute(SharedScopeKeys.USER_KEY, vo);
// ===================================================================
// 2. 자동 로그인 처리
// ===================================================================
// ===================================================================
// 1 ) 자동 로그인(rememberMe) 옵션의 on / off 여부 확인
// ===================================================================
boolean isRememberMeOption = checkRememberMeOption(req);
log.info("\t + isRememberMeOption : {}", isRememberMeOption);
if ( isRememberMeOption ) { // + 자동 로그인 기능 적용
// + Response Message의 Header에 쿠키를 저장해서 보낸다.
// + 이때 쿠키값으로는 현재 브라우저의 이름인 세션ID를 저장하자!
// + (*주의*) 세션 ID는 UUID이다.
String sessionId = session.getId();
Cookie rememberMeCookie = new Cookie(SharedScopeKeys.REMEMBER_ME_KEY, sessionId);
// + 쿠키는 전부 문자열이기에, 숫자로 보일지라도 문자열이다.
// + public Cookie(String name, String value)
rememberMeCookie.setMaxAge( 1 * 60 * 60 * 24 * 7 ); // 1주일
// + 쿠키의 유효기간( 자동 로그인 유효기간 ) 설정
// + .setMaxAge의 단위는 초이다.
rememberMeCookie.setPath("/");
// ===================================================================
// + 쿠키 확인
// ===================================================================
log.info("\t + rememberMeCookie - 1. name : {}", rememberMeCookie.getName());
log.info("\t + rememberMeCookie - 2. value : {} ", rememberMeCookie.getValue());
log.info("\t + rememberMeCookie - 3. path : {} ", rememberMeCookie.getPath());
log.info("\t + rememberMeCookie - 4. MaxAge : {} ", rememberMeCookie.getMaxAge());
res.addCookie(rememberMeCookie);
// + 응답 메세지의 'set-cookie' 헤더에 자동설정된다.
} // inner if
} // if : 성공했다면
} // postHandle
// ===================================================================================================
// + 3. afterCompletion : 주로 자원 해제하는데 사용
// ===================================================================================================
// + afterCompletion은 view처리가 끝난 후 User에게 응답이 나가기 직전에 인터셉트하게 되는데,
// + 만약 처리할게 없다면, 삭제해도 된다.
// ===================================================================================================
@Override
public void afterCompletion(HttpServletRequest req, HttpServletResponse res, Object handler, Exception e)
throws Exception {
log.info("========================================================");
log.trace("3. afterCompletion(req, res, handler, handler, e) invoked.");
log.info("========================================================");
} // afterCompletion
// ===================================================================================================
// + 4. 자동 로그인 on / off 체크 여부 확인 메소드
// ===================================================================================================
private boolean checkRememberMeOption(HttpServletRequest req) {
log.trace("checkRememberMeOption(req) invoked.");
String rememberMe = req.getParameter("rememberMe");
log.info("\t + rememberMe : {}",rememberMe);
// + 전송 파라미터 중에서 rememberMe로 들어온 값을 변수 rememberMe에 저장
return rememberMe != null;
// + rememberMe가 null이 아니면 true를 반환하고,
// + rememberMe가 null이라면 false를 반환한다.
} // checkRememberMeOption
// ===================================================================================================
} // end class
[ + 인증 / 인가 처리 인터셉터 ]
더보기
[ + 코드 보기 ]
package org.zerock.myapp.interceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.stereotype.Component;
import org.springframework.ui.ModelMap;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import org.zerock.myapp.common.SharedScopeKeys;
import org.zerock.myapp.domain.UserVO;
import lombok.NoArgsConstructor;
import lombok.extern.log4j.Log4j2;
@Log4j2
@NoArgsConstructor
@Component
public class AuthInterceptor implements HandlerInterceptor { // 인증, 인가 처리
// ===================================================================================================
// + 1. preHandle
// ===================================================================================================
// + 게시판에 관련된 모든 요청에 대해서 인증된 사용자(브라우저)인지를 가장 먼저 체크하고
// + 만약, 인증되지 않은 사용자(웹 브라우저)라면, 로그인을 하도록 로그인 창으로 리다이렉트해야 한다,
// + 위의 로직은 모든 인증 기능에서 가장 먼저 수행되어야 할 "보안 로직"이다.
// ===================================================================================================
@Override
public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object handler) throws Exception {
log.info("========================================================");
log.trace("1. preHandle(req, res, handler) invoked.");
log.info("========================================================");
// =======================================================================
// 1. 현재 요청 URI를 전송한 사용자(웹 브라우저)의 인증여부 확인
// =======================================================================
HttpSession session = req.getSession();
log.info("\t + Session ID : {}", session.getId() );
UserVO vo = (UserVO) session.getAttribute(SharedScopeKeys.USER_KEY);
// + Session Scope에 있는 공유 데이터 중 로그인 성공 데이터를 찾는다.
if ( vo != null ) { // + 인증된 사용자(웹 브라우저)라면...
log.info("\t + Already Authenticated : {}", vo);
return true;
// + 요청을 컨트롤러의 헨들러로 넘긴다.
} else { // + 인증되지 않은 사용자(웹 브라우저)라면...
log.info("\t + NO Authenticated User");
res.sendRedirect("/user/login");
// + 로그인 창으로 리다이렉트해버린다.
return false;
// + 요청을 컨트롤러의 헨들러로 넘기지 않는다.
// + 인증되지 않은 사용자의 경우에는 게시물로 접근이 불가능해야 한다.
} // if - else
} // preHandle
// ===================================================================================================
// + 2. postHandle
// ===================================================================================================
// + 만약 컨트롤러에서 예외가 발생하면, 아래의 callback 메소드는 실행되지 않는다.
@Override
public void postHandle(HttpServletRequest req, HttpServletResponse res, Object handler,
ModelAndView modelAndView) throws Exception {
log.info("========================================================");
log.trace("2. postHandle(req, res, handler, {}) invoked.", modelAndView);
log.info("========================================================");
} // postHandle
// ===================================================================================================
// + 3. afterCompletion
// ===================================================================================================
@Override
public void afterCompletion(HttpServletRequest req, HttpServletResponse res, Object handler, Exception e)
throws Exception {
log.info("========================================================");
log.trace("3. afterCompletion(req, res, handler, handler, e) invoked.");
log.info("========================================================");
} // afterCompletion
} // end class
[ + 로그아웃 처리 인터셉터 ]
더보기
[ + 코드 보기 ]
package org.zerock.myapp.interceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import org.zerock.myapp.common.SharedScopeKeys;
import lombok.NoArgsConstructor;
import lombok.extern.log4j.Log4j2;
@Log4j2
@NoArgsConstructor
@Component
public class LogoutInterceptor implements HandlerInterceptor {
// ===================================================================================================
// + 1. preHandle
// ===================================================================================================
@Override
public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object handler) throws Exception {
log.info("========================================================");
log.trace("1. preHandle(req, res, handler) invoked.");
log.info("========================================================");
// + 1. 현재 브라우저에 할당된 Session Scope를 모두 파괴하고, ( 즉, 로그인 성공증빙도 파괴 )
// + 웹 브라우저에 할당된 이름( Session ID )도 무효화시킨다.
HttpSession session = req.getSession();
String sessionId = session.getId();
log.info("\t + sessionID : {}", sessionId);
session.invalidate();
log.info("\t + Current Session Scope Destroyed.");
req.setAttribute(SharedScopeKeys.RESULT, "Signed Out Successfully");
// + rttrs.addAttribute(key, value)와 동일한 기능을 한다.
res.sendRedirect("/user/login");
// + 최종적으로 로그인 화면 창으로 리다이렉트 수행
// + Redirect할 경우에는 Session Scope이 파괴된다는 것을 잊지 말아야 한다.
// + 그렇기에, 로드인 페이지에서는 Signed Out Successfully 메시지를 얻어낼 수 없게 된다.
return false;
// + 로그아웃 요청을 컨트롤러의 핸들러로 요청을 보내지 않는다. (***)
// + 그러나, URI는 필요하기에 컨트롤러의 메소드를 삭제하면 인터셉터도 수행이 불가능하다.
// + 로그인 컨트롤러의 로그아웃 메소드는 단순히 형식으로만 존재하게 된다.
} // preHandle
} // end class
[ 1 - 8. 인터셉터 등록 ]
더보기
[ + 코드 보기 ]
<!-- 인터셉터 등록 방법 (****) -->
<interceptors>
<interceptor>
<mapping path="/user/loginPost" />
<beans:ref bean="loginInterceptor" />
</interceptor>
<interceptor>
<mapping path="/user/logout" />
<beans:ref bean="logoutInterceptor" />
</interceptor>
<interceptor>
<mapping path="/board/*" />
<beans:ref bean="authInterceptor" />
</interceptor>
</interceptors>
728x90
'KH 정보교육원 [ Java ]' 카테고리의 다른 글
KH 126일차 - Spring ( AOP ) (***) (0) | 2022.08.26 |
---|---|
KH 125일차 - Spring( 인터셉터 3 - 세션 트레킹 : 자동 로그인 ) (******) (0) | 2022.08.25 |
KH 123일차 - Spring ( 인터셉터 ) (***) (0) | 2022.08.24 |
KH 122일차 - Spring ( Restful 방식 2 / 인터셉터 ) (****) (0) | 2022.08.22 |
KH 121일차 - Spring ( Restful 방식 ) (****) (0) | 2022.08.19 |
댓글