ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Camel][Spring] 게시판 만들기 #9. 회원가입, 로그인 기능 구현
    Spring/게시판 만들기 2020. 6. 29. 01:30

    [Camel][Spring] 게시판 만들기 #9. 회원가입, 로그인 기능 구현

     

    본 게시판 만들기 프로젝트는 더블에스 Devlog Spring-MVC 를 참조하여 작성했음을 알려드립니다. 또한 개인적인 학습을 목적으로한 포스팅이기 때문에 완벽하지 않을 수 있음을 알려드립니다. 문제점이나 궁금한점은 댓글로 남겨주시면 감사하겠습니다. 프로젝트 생성에 앞서 이번 게시판 만들기 프로젝트는 이클립스를 사용하여 구현하였습니다. 

     

    이번 포스팅에서는 회원가입과 로그인 기능을 구현해 보겠습니다. 웹에서의 로그인의 일반적인 방식은 HttpSession 객체를 사용해 사용자의 정보를 보관하고, 필요한 경우 그 정보를 사용하고 수정하는 방식을 사용합니다. 

     

    1. HttpSession 

    Session 이란 서버가 해당 웹에 접근한 사용자를 식별하는 방법입니다. 웹에서는 접근한 사용자에게 Response Header 필드인 ser-cookie 값으로 사용자를 식별하는 Session Id를 부여합니다. 웹으로부터 부여된 Session Id는 해당 웹(서버)과 브라우저(클라이언트) 메모리에 저장됩니다. 이때 클라이언트 메모리에 사용되는 Cookie의 타입은 Session이 종료될 때 함께 소멸되는 메모리 Cookie가 사용됩니다. 

     

    이러한 Session 을 이용하는 방식의 핵심은 HttpSession을 이용해 원하는 객체를 보관할 수 있다는 점입니다. 사용자는 항상 Session Cookie를 사용해 접근하고,  서버의 내부에 HttpSession 객체를 보관하기 때문에 보안상 안전하게 됩니다. 

     

     

    2. 회웝가입 기능 구현

    2-1. User 테이블 생성

    로그인 기능을 구현하기 위해서는 회원가입 기능이 필수적으로 필요하며, 회원가입과 로그인을 처리하기 위해서 User 테이블을 생성하겠습니다. 아래의 SQL문과 같이 테이블을 생성해보겠습니다. 

    CREATE TABLE tb_user (
      user_id VARCHAR(50) NOT NULL,
      user_pw VARCHAR(100) NOT NULL,
      user_name VARCHAR(100) NOT NULL,
      user_email VARCHAR(50) NOT NULL,
      user_point INT NOT NULL DEFAULT 0,
      session_key VARCHAR(50) NOT NULL DEFAULT 'none',
      session_limit TIMESTAMP,
      user_img VARCHAR(100) NOT NULL DEFAULT 'user/default-user.png',
      user_join_date TIMESTAMP NOT NULL DEFAULT NOW(),
      user_login_date TIMESTAMP NOT NULL DEFAULT NOW(),
      user_signature VARCHAR(200) NOT NULL DEFAULT 'Hello...',
      PRIMARY KEY (user_id)
    );

     

    2-2. UserVO 생성

    앞서 생성한 테이블의 구조를 객체화 시키기 위해 UserVO 클래스를 src/main/java/기본패키지/domain 경로에 생성한 후 아래와 같이 작성해 줍니다. 

    public class UserVO {
    
        private String userId;
        private String userPw;
        private String userName;
        private String userEmail;
        private Date userJoinDate;
        private Date userLoginDate;
        private String userSignature;
        private String userImg;
        private int userPoint;
    
        // Getter, Setter, toString 은 생략하겠습니다.
    }

     

    2-3. Persistence 계층 구현

    src/main/java/기본패키지/persistence 경로에 UserDAO 인터페이스와 UserDAOImpl 클래스를 생성해줍니다. UserDAO 인터페이스에는 회원가입 처리를 위한 추상메소드를 정의해주고, UserDAOImpl 클래스에서 구현해주겠습니다. 

    public interface UserDAO {
    
        // 회원가입 처리
        void register(UserVO userVO) throws Exception;
    
    }
    package com.cameldev.mypage.persistence;
    
    import javax.inject.Inject;
    
    import org.apache.ibatis.session.SqlSession;
    import org.springframework.stereotype.Repository;
    
    import com.cameldev.mypage.domain.UserVO;
    
    @Repository
    public class UserDAOImpl implements UserDAO {
    
        private static final String NAMESPACE = "com.cameldev.mypage.mappers.user.UserMapper";
    
        private final SqlSession sqlSession;
    
        @Inject
        public UserDAOImpl(SqlSession sqlSession) {
            this.sqlSession = sqlSession;
        }
    
        // 회원가입처리
        @Override
        public void register(UserVO userVO) throws Exception {
            sqlSession.insert(NAMESPACE + ".register", userVO);
        }
    
    }

     

    이어서 src/main/resources/mappers/user/경로에 userMapper.xml을 생성하고, 아래와 같이 회원가입을 위한 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="com.cameldev.mypage.mappers.user.UserMapper">
    
        <insert id="register">
            INSERT INTO tb_user (
                user_id
                , user_pw
                , user_name
                , user_email
            ) VALUES (
                #{userId}
                , #{userPw}
                , #{userName}
                , #{userEmail}
            )
        </insert>
    
    </mapper>

     

     

    2-4. Service 계층 구현

    src/main/java/기본패키지/service경로에 UserService 인터페이스와 UserServiceImpl 클래스를 생성해줍니다. UserService 인터페이스에도 회원가입 처리를 위한 추상메소드를 정의해주고, UserServiceImpl 클래스에서 구현해주겠습니다. 

    public interface UserService {
    
        // 회원 가입 처리
        void register(UserVO userVO) throws Exception;
    
    }
    package com.cameldev.mypage.service;
    
    import javax.inject.Inject;
    
    import org.springframework.stereotype.Service;
    
    import com.cameldev.mypage.domain.UserVO;
    import com.cameldev.mypage.persistence.UserDAO;
    
    @Service
    public class UserServiceImpl implements UserService {
    
        private final UserDAO userDAO;
    
        @Inject
        public UserServiceImpl(UserDAO userDAO) {
            this.userDAO = userDAO;
        }
    
        // 회원 가입 처리
        @Override
        public void register(UserVO userVO) throws Exception {
            userDAO.register(userVO);
        }
    
    }

     

    2-5. UserController 작성 ( UserRegisterController )

    User관련 URI를 통합해서 하나의 컨트롤러에 전부 매핑시키는 것도 가능하지만, 코드가 많아지고 구분이 어려워지기 때문에 게시글 컨트롤러는 구분했던것 처럼 회원 가입페이지, 가입처리, 탈퇴 uri만을 매핑하는 컨트롤러를 따로 만들어 사용하겠습니다. 

     

    src/main/java/기본패키지/controller 경로에 UserRegisterController 를 생성하고 아래와 같이 작성하겠습니다. 

    package com.cameldev.mypage.controller;
    
    import javax.inject.Inject;
    
    import org.mindrot.jbcrypt.BCrypt;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    import org.springframework.web.servlet.mvc.support.RedirectAttributes;
    
    import com.cameldev.mypage.domain.UserVO;
    import com.cameldev.mypage.service.UserService;
    
    @Controller
    @RequestMapping("/user")
    public class UserRegisterController {
    
        private final UserService userService;
    
        @Inject
        public UserRegisterController(UserService userService) {
            this.userService = userService;
        }
    
        // 회원가입 페이지
        @RequestMapping(value = "/register", method = RequestMethod.GET)
        public String registerGET() throws Exception {
            return "/user/register";
        }
    
        // 회원가입 처리
        @RequestMapping(value = "/register", method = RequestMethod.POST)
        public String registerPOST(UserVO userVO, RedirectAttributes redirectAttributes) throws Exception {
    
            String hashedPw = BCrypt.hashpw(userVO.getUserPw(), BCrypt.gensalt());
            userVO.setUserPw(hashedPw);
            userService.register(userVO);
            redirectAttributes.addFlashAttribute("msg", "REGISTERED");
    
            return "redirect:/user/login";
        }
        
        // 로그인 페이지 (임시로 여기에 작성하고 추후 UserLoginController에서 다시 작성)
        @RequestMapping(value = "/login", method = RequestMethod.GET)
        public String login() throws Exception {
            return "/user/login";
        }
    
    }

    위의 코드에서 주목해서 봐야할 점은 회원가입 처리 매핑 메서드인 registerPost( )입니다. 이 메소드는 파라미터로 넘어온 회원의 객체정보(UserVO) 중에서 비밀번호(userPw)를 암호화 하는 작업을 수행합니다. 이렇게 하는 이유는 비밀번호는 DB에 암호화해서 보관해야 보안에 비교적 안전하기 때문입니다. BCrypt.hashpw()메서드는 위와 같이 첫번째 파라미터에는 암호화할 비밀번호를 두번째 파라미터는 BCrypt.gensalt()를 받고 암호화된 비밀번호를 반환해줍니다. 이렇게 암호화된 비밀번호를 다시 회원 객체에 저장하고 서비스의 회원가입 메서드를 호출하는 것입니다. 

     

    이러한 BCrypt 를 사용하기 위해서는 pom.xml에 아래의 라이브러리를 추가해주어야 합니다. 

    <!--비밀번호 암호화 -->
    <!-- https://mvnrepository.com/artifact/org.mindrot/jbcrypt -->
    <dependency>
        <groupId>org.mindrot</groupId>
        <artifactId>jbcrypt</artifactId>
        <version>0.4</version>
    </dependency>

     

    2-5. register.jsp & login.jsp 생성

    WEB-INF/views/user 디렉토리를 생성하고 그 안에 register.jsp & login.jsp 파일을 생성하겠습니다. 

     

    register.jsp 파일은 아래와 같이 작성하도록 하겠습니다.

    <%@ page contentType="text/html; charset=UTF-8" language="java" %>
    <!DOCTYPE html>
    <html>
    <%@ include file="../include/head.jsp" %>
    <body class="hold-transition register-page">
    <div class="register-box">
      <div class="register-logo">
        <a href="${path}/"><b>Admin</b>LTE</a>
      </div>
    
      <div class="card">
        <div class="card-body register-card-body">
          <p class="login-box-msg">Register a new membership</p>
    
          <form action="${path}/user/register" method="post">
            <div class="input-group mb-3">
              <input type="text" name="userId" class="form-control" placeholder="아아디">
              <div class="input-group-append">
                <div class="input-group-text">
                  <span class="fas fa-exclamation"></span>
                </div>
              </div>
            </div>
            <div class="input-group mb-3">
              <input type="text" name="userName" class="form-control" placeholder="이름">
              <div class="input-group-append">
                <div class="input-group-text">
                  <span class="fas fa-user"></span>
                </div>
              </div>
            </div>
            <div class="input-group mb-3">
              <input type="email" name="userEmail" class="form-control" placeholder="이메일">
              <div class="input-group-append">
                <div class="input-group-text">
                  <span class="fas fa-envelope"></span>
                </div>
              </div>
            </div>
            <div class="input-group mb-3">
              <input type="password" name="userPw" class="form-control" placeholder="비밀번호">
              <div class="input-group-append">
                <div class="input-group-text">
                  <span class="fas fa-lock"></span>
                </div>
              </div>
            </div>
            <div class="input-group mb-3">
              <input type="password" class="form-control" placeholder="비밀번호 확인">
              <div class="input-group-append">
                <div class="input-group-text">
                  <span class="fas fa-lock"></span>
                </div>
              </div>
            </div>
            <div class="row">
              <div class="col-8">
                <div class="icheck-primary">
                  <input type="checkbox" id="agreeTerms" name="terms" value="agree">
                  <label for="agreeTerms">
                   I agree to the <a href="#">terms</a>
                  </label>
                </div>
              </div>
              <!-- /.col -->
              <div class="col-4">
                <button type="submit" class="btn btn-primary btn-block">Register</button>
              </div>
              <!-- /.col -->
            </div>
          </form>
    
          <a href="${path}/user/login" class="text-center">I already have a membership</a>
        </div>
        <!-- /.form-box -->
      </div><!-- /.card -->
    </div>
    <!-- /.register-box -->
    
    <%@ include file="../include/plugin_js.jsp" %>
    <script>
        $(function () {
            $('input').iCheck({
                checkboxClass: 'icheckbox_square-blue',
                radioClass: 'iradio_square-blue',
                increaseArea: '20%' // optional
            });
        });
    </script>
    </body>
    </html>

     

    login.jsp 파일은 아래와 같이 작성하겠습니다. 

    <%@ page contentType="text/html; charset=UTF-8" language="java" %>
    <!DOCTYPE html>
    <html>
    <%@ include file="../include/head.jsp" %>
    <body class="hold-transition login-page">
    <div class="login-box">
        <div class="login-logo">
    	    <a href="${path}/"><b>Admin</b>LTE</a>
    	</div>
        <!-- /.login-logo -->
        <div class="card">
    	    <div class="card-body login-card-body">
    	      <p class="login-box-msg">Sign in to start your session</p>
    	
    	      <form action="${path}/user/loginPost" method="post">
    	        <div class="input-group mb-3">
    	          <input type="text" name="userId" class="form-control" placeholder="아아디">
    	          <div class="input-group-append">
    	            <div class="input-group-text">
    	              <span class="fas fa-exclamation"></span>
    	            </div>
    	          </div>
    	        </div>
    	        <div class="input-group mb-3">
    	          <input type="password" name="userPw" class="form-control" placeholder="비밀번호">
    	          <div class="input-group-append">
    	            <div class="input-group-text">
    	              <span class="fas fa-lock"></span>
    	            </div>
    	          </div>
    	        </div>
    	        <div class="row">
    	          <div class="col-8">
    	            <div class="icheck-primary">
    	              <input type="checkbox" id="remember">
    	              <label for="remember">
    	                Remember Me
    	              </label>
    	            </div>
    	          </div>
    	          <!-- /.col -->
    	          <div class="col-4">
    	            <button type="submit" class="btn btn-primary btn-block">Sign In</button>
    	          </div>
    	          <!-- /.col -->
    	        </div>
    	      </form>
    	
    	      <p class="mb-1">
    	        <a href="#">I forgot my password</a>
    	      </p>
    	      <p class="mb-0">
    	        <a href="${path}/user/register" class="text-center">Register a new membership</a>
    	      </p>
    	    </div>
        <!-- /.login-card-body -->
      </div>
        <!-- /.login-box-body -->
    </div>
    <!-- /.login-box -->
    
    <%@ include file="../include/plugin_js.jsp" %>
    <script>
        var msg = "${msg}";
        if (msg === "REGISTERED") {
            alert("회원가입이 완료되었습니다. 로그인해주세요~");
        } else if (msg == "FAILURE") {
            alert("아이디와 비밀번호를 확인해주세요.");
        }
        $(function () {
            $('input').iCheck({
                checkboxClass: 'icheckbox_square-blue',
                radioClass: 'iradio_square-blue',
                increaseArea: '20%' // optional
            });
        });
    </script>
    </body>
    </html>

     

    여기까지 모두 완료하셨다면 회원가입 기능을 정상적으로 처리될 것입니다. 당연히 데이터베이스에도 회원 정보가 입력되는 것을 확인할 수 있습니다. 데이터베이스에 저장되 회원 비밀번호를 확인해보면 암호화 되어 저장된 것을 확인 할수 수 있습니다. 

     

     

    3. 로그인 기능 구현

    3-1. 로그인 처리를 위한 LoginDTO 클래스 생성

    src/main/java/기본패키지/domain 경로에 LoginDTO 클래스를 생성한 뒤, 아래와 같이 작석해줍니다. LoginDTO를 따로 만드는 이유는 로그인 화면으로부터 전달되는 회원의 데이터를 수집하기 위함입니다. 

     

    package com.cameldev.mypage.domain;
    
    public class LoginDTO {
    
        private String userId;
        private String userPw;
        private boolean useCookie;
        
    	public String getUserId() {
    		return userId;
    	}
    	public void setUserId(String userId) {
    		this.userId = userId;
    	}
    	public String getUserPw() {
    		return userPw;
    	}
    	public void setUserPw(String userPw) {
    		this.userPw = userPw;
    	}
    	public boolean isUseCookie() {
    		return useCookie;
    	}
    	public void setUseCookie(boolean useCookie) {
    		this.useCookie = useCookie;
    	}
        
    	@Override
        public String toString() {
            return "LoginDTO{" +
                    "userId='" + userId + '\'' +
                    ", userPw='" + userPw + '\'' +
                    ", useCookie=" + useCookie +
                    '}';
        }
    
    }

     

     

    3-2. Persistence 계층 구현

    src/main/java/기본패키지/persistence 경로에 UserDAO 인터페이스와 UserDAOImpl 클래스에 아래의 내용을 추가해주겠습니다. UserDAO 인터페이스에 로그인 처리를 위한 추상메소드를 정의해주고, UserDAOImpl 클래스에서 구현해주겠습니다. 

    public interface UserDAO {
    
        // 회원가입 처리
        void register(UserVO userVO) throws Exception;
    	
        // 로그인 처리
        UserVO login(LoginDTO loginDTO) throws Exception;
        
    }
    package com.cameldev.mypage.persistence;
    
    import javax.inject.Inject;
    
    import org.apache.ibatis.session.SqlSession;
    import org.springframework.stereotype.Repository;
    
    import com.cameldev.mypage.domain.UserVO;
    
    @Repository
    public class UserDAOImpl implements UserDAO {
    
        private static final String NAMESPACE = "com.cameldev.mypage.mappers.user.UserMapper";
    
        private final SqlSession sqlSession;
    
        @Inject
        public UserDAOImpl(SqlSession sqlSession) {
            this.sqlSession = sqlSession;
        }
    
        // 회원가입처리
        @Override
        public void register(UserVO userVO) throws Exception {
            sqlSession.insert(NAMESPACE + ".register", userVO);
        }
        
        // 로그인 처리
        @Override
        public UserVO login(LoginDTO loginDTO) throws Exception {
            return sqlSession.selectOne(NAMESPACE + ".login", loginDTO);
        }
    
    }

     

    이어서 src/main/resources/mappers/user/경로에 userMapper.xml을 생성하고, 아래와 같이 회원가입을 위한 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="com.cameldev.mypage.mappers.user.UserMapper">
    
        <insert id="register">
            INSERT INTO tb_user (
                user_id
                , user_pw
                , user_name
                , user_email
            ) VALUES (
                #{userId}
                , #{userPw}
                , #{userName}
                , #{userEmail}
            )
        </insert>
        
        <select id="login" resultMap="userVOResultMap">
        	SELECT
        	  *
        	FROM tbl_user
        	WHERE user_id = #{userId}
        </select>
    
    </mapper>

     

     

    3-3. Service 계층 구현

    src/main/java/기본패키지/service경로에 UserService 인터페이스와 UserServiceImpl 클래스를 작성해줍니다. UserService 인터페이스에도 로그인 처리를 위한 추상메소드를 정의해주고, UserServiceImpl 클래스에서 구현해주겠습니다. 

    public interface UserService {
    
        // 회원 가입 처리
        void register(UserVO userVO) throws Exception;
        
        // 로그인 처리
        UserVO login(LoginDTO loginDTO) throws Exception;
    
    }
    package com.cameldev.mypage.service;
    
    import javax.inject.Inject;
    
    import org.springframework.stereotype.Service;
    
    import com.cameldev.mypage.domain.UserVO;
    import com.cameldev.mypage.persistence.UserDAO;
    
    @Service
    public class UserServiceImpl implements UserService {
    
        private final UserDAO userDAO;
    
        @Inject
        public UserServiceImpl(UserDAO userDAO) {
            this.userDAO = userDAO;
        }
    
        // 회원 가입 처리
        @Override
        public void register(UserVO userVO) throws Exception {
            userDAO.register(userVO);
        }
        
        // 로그인 처리
        @Override
    	public UserVO login(LoginDTO loginDTO) throws Exception {
        	return userDAO.login(loginDTO);
        }
    
    }

     

    3-4. UserController 작성(UserLoginController)

    앞서 언급했다시피 User관련 URI를 통합해서 하나의 컨트롤러에 전부 매핑시키는 것도 가능하지만, 코드가 많아지고 구분이 어려워지기 때문에 게시글 컨트롤러는 구분했던것 처럼 회원 가입페이지, 가입처리, 탈퇴 uri만을 매핑하는 컨트롤러를 따로 만들어 사용하겠습니다. 

     

    src/main/java/기본패키지/controller 경로에 UserLoginController 를 생성하고 아래와 같이 작성하겠습니다. 

    package com.cameldev.mypage.controller;
    
    import javax.inject.Inject;
    import javax.servlet.http.HttpSession;
    
    import org.mindrot.jbcrypt.BCrypt;
    import org.springframework.stereotype.Controller;
    import org.springframework.ui.Model;
    import org.springframework.web.bind.annotation.ModelAttribute;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    
    import com.cameldev.mypage.domain.LoginDTO;
    import com.cameldev.mypage.domain.UserVO;
    import com.cameldev.mypage.service.UserService;
    
    @Controller
    @RequestMapping("/user")
    public class UserLoginController {
    
        private final UserService userService;
    
        @Inject
        public UserLoginController(UserService userService) {
            this.userService = userService;
        }
    
        // 로그인 페이지
        @RequestMapping(value = "/login", method = RequestMethod.GET)
        public String loginGET(@ModelAttribute("loginDTO") LoginDTO loginDTO) {
            return "/user/login";
        }
    
        // 로그인 처리
        @RequestMapping(value = "/loginPost", method = RequestMethod.POST)
        public void loginPOST(LoginDTO loginDTO, HttpSession httpSession, Model model) throws Exception {
    
            UserVO userVO = userService.login(loginDTO);
    
            if (userVO == null || !BCrypt.checkpw(loginDTO.getUserPw(), userVO.getUserPw())) {
                return;
            }
    
            model.addAttribute("user", userVO);
    
        }
    
    }

     

    3-5.로그인 Interceptor 클래스 작성 및 설정

    src/main/java/기본패키지/commons/interceptor 디렉터리를 생성하고 LoginInterceptor 클래스를 생성합니다. UserLoginController 클래스에서 HttpSession과 관련된 작업을 처리하지 않았기 때문에 HttpSession과 관련된 모든 설정을 인터셉터에서 처리하겠습니다. 

    package com.cameldev.mypage.commons.interceptor;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import javax.servlet.http.HttpSession;
    
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.ui.ModelMap;
    import org.springframework.web.servlet.ModelAndView;
    import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
    
    public class LoginInterceptor extends HandlerInterceptorAdapter {
    
        private static final String LOGIN = "login";
        private static final Logger logger = LoggerFactory.getLogger(LoginInterceptor.class);
    
        @Override
        public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
    
            HttpSession httpSession = request.getSession();
            ModelMap modelMap = modelAndView.getModelMap();
            Object userVO =  modelMap.get("user");
    
            if (userVO != null) {
                logger.info("new login success");
                httpSession.setAttribute(LOGIN, userVO);
                response.sendRedirect("/mypage");
            }
    
        }
    
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    
            HttpSession httpSession = request.getSession();
            // 기존의 로그인 정보 제거
            if (httpSession.getAttribute(LOGIN) != null) {
                logger.info("clear login data before");
                httpSession.removeAttribute(LOGIN);
            }
    
            return true;
        }
    }

     

    LoginInterceptor의 postHandle() 메서드는 httpSession에 컨트롤러에서 저장한 user를 저장하고, /로 리다이렉트를 한다. 그리고 preHandle() 메서드는 기존의 로그인 정보가 있을 경우 초기화하는 역할을 수행한다.

    앞서 작성한 로그인 인터셉터 클래스를 스프링에서 인터셉터로 인식시키기 위해 아래와 같이 servlet-context.xml에 코드를 추가해줍니다.

    <beans:bean id="loginInterceptor" class="com.cameldev.mypage.commons.interceptor.LoginInterceptor"/>
    	
    <interceptors>
        <interceptor>
          <mapping path="/user/loginPost"/>
          <beans:ref bean="loginInterceptor"/>
        </interceptor>
    </interceptors>

     

    3-6. 로그인 화면 작성

     로그인 화면은 이미 회원가입 처리를 구현하면서 이미 작성했고, 결과 페이지에 해당하는 loginPost.jsp를 아래와 같이 작성해주겠습니다. 컨트롤러에서 만약 회원정보가 없거나, 비밀번호가 불일치한다면 loginPost.jsp로 이동하여 아이디와 비밀번호를 확인하라는 메시지와 함께 로그인페이지로 다시 이동하게 처리하였습니다.

    <%@ page contentType="text/html;charset=UTF-8" language="java" %>
    <html>
    <head>
        <title>Title</title>
    </head>
    <body>
        <script>
            alert("아이디와 비밀번호를 확인해주세요.");
            self.location = "${path}/user/login";
        </script>
    </body>
    </html>

     

    다음으로는 로그인처리가 되었는지 사용자가 알 수 있도록 include한 페이지들(main_header.jsp, left_column.jsp)를 아래와 같이 수정해주겠습니다.

    <%@ page contentType="text/html; charset=UTF-8" language="java" %>
    <nav class="main-header navbar navbar-expand navbar-white navbar-light">
        <!-- Left navbar links -->
        <ul class="navbar-nav">
          <li class="nav-item">
            <a class="nav-link" data-widget="pushmenu" href="#" role="button"><i class="fas fa-bars"></i></a>
          </li>
          <li class="nav-item d-none d-sm-inline-block">
            <a href="index3.html" class="nav-link">Home</a>
          </li>
          <li class="nav-item d-none d-sm-inline-block">
            <a href="#" class="nav-link">Contact</a>
          </li>
        </ul>
    
        <!-- SEARCH FORM -->
        <form class="form-inline ml-3">
          <div class="input-group input-group-sm">
            <input class="form-control form-control-navbar" type="search" placeholder="Search" aria-label="Search">
            <div class="input-group-append">
              <button class="btn btn-navbar" type="submit">
                <i class="fas fa-search"></i>
              </button>
            </div>
          </div>
        </form>
    
        <!-- Right navbar links -->
         <ul class="navbar-nav ml-auto">
                    <c:if test="${not empty login}">
                    <li class="nav-item dropdown user user-menu">
                        <a href="#" class="dropdown-toggle" data-toggle="dropdown">
                            <img src="${path}/dist/img/default-user-image.jpg" class="user-image" alt="User Image">
                            <span class="hidden-xs">${login.userName}</span>
                        </a>
                        <ul class="dropdown-menu">
                            <li class="user-header">
                                <img src="${path}/dist/img/default-user-image.jpg" class="img-circle" alt="User Image">
                                <p>
                                    <small>
                                        가입일자 : <fmt:formatDate value="${login.userJoinDate}" pattern="yyyy-MM-dd"/>
                                    </small>
                                    <small>
                                        최근로그인일자 : <fmt:formatDate value="${login.userLoginDate}" pattern="yyyy-MM-dd"/>
                                    </small>
                                </p>
                            </li>
                            <li class="user-footer">
                                <div class="float-left">
                                    <a href="${path}/user/profile" class="btn btn-default btn-flat"><i
                                            class="fa fa-info-circle"></i><b> 내 프로필</b></a>
                                </div>
                                <div class="float-right">
                                    <a href="${path}/user/logout" class="btn btn-default btn-flat"><i
                                            class="glyphicon glyphicon-log-out"></i><b> 로그아웃</b></a>
                                </div>
                            </li>
                        </ul>
                    </li>
                    </c:if>
                    <c:if test="${empty login}">
                    <li class="nav-item dropdown user user-menu">
                        <a href="#" class="dropdown-toggle" data-toggle="dropdown">
                            <img src="${path}/dist/img/default-user-image.jpg" class="user-image" alt="User Image">
                            <span class="hidden-xs">회원가입 또는 로그인</span>
                        </a>
                        <ul class="dropdown-menu">
                            <li class="user-header">
                                <img src="${path}/dist/img/default-user-image.jpg" class="img-circle" alt="User Image">
                                <p>
                                    <b>회원가입 또는 로그인해주세요</b>
                                    <small></small>
                                </p>
                            </li>
                            <li class="user-footer">
                                <div class="float-left">
                                    <a href="${path}/user/register" class="btn btn-default btn-flat"><i
                                            class="fas fa-user-plus"></i><b> 회원가입</b></a>
                                </div>
                                <div class="float-right">
                                    <a href="${path}/user/login" class="btn btn-default btn-flat"><i
                                            class="glyphicon glyphicon-log-in"></i><b> 로그인</b></a>
                                </div>
                            </li>
                        </ul>
                    </li>
                    </c:if>
                </ul>
    
      </nav>
    <!-- Sidebar user panel (optional) -->
          <div class="user-panel mt-3 pb-3 mb-3 d-flex">
            <c:if test="${empty login}">
    	        <div class="image">
    	          <img src="${path}/dist/img/default-user-image.jpg" class="img-circle elevation-2" alt="User Image">
    	        </div>
    	        <div class="info">
                    <%-- Status --%>
                	<a href="#"><i class="fa fa-circle text-danger"></i> Guest</a>
    	        </div>
            </c:if>
            
            <c:if test="${not empty login}">
    	        <div class="image">
    	          <img src="${path}/dist/img/default-user-image.jpg" class="img-circle elevation-2" alt="User Image">
    	        </div>
    	        <div class="info">
                    <%-- Status --%>
                	<a href="#"><i class="d-block"></i> ${login.userName}</a>
    	        </div>
            </c:if>
          </div>

     

     

    3-7. 권한을 위한  AuthInterceptor 클래스 작성

    이번에 작성하는 인터셉터는 특정 경로에 접근할 경우 현재 사용자의 로그인 여부를 체크하는 역할을 수행합니다. 단전인 예로 만약 로그인이 되어있지 않다면 글쓰기 기능을 사용할 수 없게 하는 것입니다. 

    public class AuthInterceptor extends HandlerInterceptorAdapter {
    
        private static final Logger logger = LoggerFactory.getLogger(AuthInterceptor.class);
    
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    
            HttpSession httpSession = request.getSession();
    
            if (httpSession.getAttribute("login") == null) {
                logger.info("current user is not logged");
                response.sendRedirect("/mypage/user/login");
                return false;
            }
    
            return true;
        }
    }

    preHandle()메서드는 현재 사용자의 로그인 여부를 확인하고, 컨트롤러를 호출할 것인지 아닌지를 결정합니다. 그리고 만약 로그인하지 않은 사용자라면 로그인 페이지로 리다이렉트하게 처리했습니다. 

     

     AuthInterceptor클래스를 인터셉터로 인식하기 위해 servlet-context.xml에 아래와 같이 코드를 추가해줍니다. 게시물 입력, 수정, 삭제, 회원 정보 페이지 요청에는 권한 인터셉터가 작동하도록 아래와 같이 매핑을 해주었습니다.

    <beans:bean id="loginInterceptor" class="com.cameldev.mypage.commons.interceptor.LoginInterceptor"/>
    	<beans:bean id="authInterceptor" class="com.cameldev.mypage.commons.interceptor.AuthInterceptor"/>
    	
    	<interceptors>
            <interceptor>
                <mapping path="/user/loginPost"/>
                <beans:ref bean="loginInterceptor"/>
            </interceptor>
            <interceptor>
                <mapping path="/article/paging/search/write"/>
                <mapping path="/article/paging/search/modify"/>
                <mapping path="/article/paging/search/remove"/>
                <mapping path="/user/info"/>
                <beans:ref bean="authInterceptor"/>
            </interceptor>
        </interceptors>

     

    이제 비로그인 상태에서 글 쓰기를 시도하거나 게시글 수정 및 삭제를 시도할 경우 로그인 페이지로 이동하는 것을 확인할 수 있습니다.

     하지만 한가지 아쉬운 점은 사용자가 윈하던 페이지로 바로 이동할 수 있게 하지 않았다는 점입니다. 이를 해결하가 위해 AuthInterceptor에서 사용자가 원하던 페이지가 무엇이었는지 보관했다가, 로그인 성공 후 해당 페이지로 이동시켜주도록 하겠습니다. 우선 AuthInterceptor 클래스를 수정해주어야하고, 이를 위해 saveDestination 이라는 메소드를 생성하겠습니다. 이 메소드는 말 그대로 목적지의 페이지 정보를 저장해두는 함수입니다. 

     

    public class AuthInterceptor extends HandlerInterceptorAdapter {
    
        private static final Logger logger = LoggerFactory.getLogger(AuthInterceptor.class);
    
        // 페이지 요청 정보 저장
        private void saveDestination(HttpServletRequest request) {
            String uri = request.getRequestURI();
            String query = request.getQueryString();
            if (query == null || query.equals("null")) {
                query = "";
            } else {
                query = "?" + query;
            }
    
            if (request.getMethod().equals("GET")) {
                logger.info("destination : " + (uri + query));
                request.getSession().setAttribute("destination", uri + query);
            }
        }
    
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    
            HttpSession httpSession = request.getSession();
    
            if (httpSession.getAttribute("login") == null) {
                logger.info("current user is not logged");
                saveDestination(request);
                response.sendRedirect("/mypage/user/login");
                return false;
            }
    
            return true;
        }
    }

    이어서 LoginInterceptor의 sendRedirect 부분을 아래와 같이 수정하겠습니다. AuthInterceptor에서 저장한 destination을 사용하기 위함입니다. 

    @Override
        public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
    
            HttpSession httpSession = request.getSession();
            ModelMap modelMap = modelAndView.getModelMap();
            Object userVO =  modelMap.get("user");
    
            if (userVO != null) {
                logger.info("new login success");
                httpSession.setAttribute(LOGIN, userVO);
                Object destination = httpSession.getAttribute("destination");
                response.sendRedirect(destination != null ? (String) destination : "/mypage");
            }
    
        }

     

     

    3-7. 게시글 수정 및 삭제에 Interceptor 적용

     

    로그인 기능을 구현했다면 이제 더이상 아무나 게시글을 수정하고 삭제할 수 없고 로그인한 사용자만 가능하도록 처리를 해주어야합니다. 

     

    비로그인 상태인 게스트는 게시글 목록을 볼 수 있고, 게시글을 조회할 수 있으며 댓글도 볼 수 있습니다. 하지만 이제는 더이상 게시글 등록,수정,삭제 그리고 댓글 등록, 수정, 삭제는 불가능하도록 구현하겠습니다.

     

    우선 게시글 등록 페이지를 아래와 같이 수정하여 작성자를 입력할 수 없도록 하겠습니다. 

    <div class="form-group" hidden>
        <label for="writer">작성자</label>
        <input class="form-control" id="writer" name="writer" value="${login.userId}" readonly>
    </div>

    그리고 게시글 조회 페이지에서는 로그인한 사용자만 수정 및 삭제가 가능하도록 아래와 같이 수정하겠습니다. 

    <div class="box-footer">
        <form role="form" method="post">
            <input type="hidden" name="article_no" value="${article.article_no}">
            <input type="hidden" name="page" value="${searchCriteria.page}">
            <input type="hidden" name="perPageNum" value="${searchCriteria.perPageNum}">
            <input type="hidden" name="searchType" value="${searchCriteria.searchType}">
            <input type="hidden" name="keyword" value="${searchCriteria.keyword}">
        </form>
        <button type="submit" class="btn btn-primary listBtn"><i class="fa fa-list"></i> 목록</button>
        <c:if test="${login.userId == article.writer}">
            <div class="pull-right">
                <button type="submit" class="btn btn-warning modBtn"><i class="fa fa-edit"></i> 수정</button>
                <button type="submit" class="btn btn-danger delBtn"><i class="fa fa-trash"></i> 삭제</button>
            </div>
        </c:if>
    </div>

    이어서 댓글 등록,수정 및 삭제 또한 로그인 상태에 영향을 받도록 하기위해 아래와 같이 댓글 입력 영역을 아래와 같이 수정하겠습니다. 

    <div class="card">
      <div class="card-body">
        <c:if test="${not empty login}">
          <form class="form-horizontal">
            <div class="row">
              <div class="form-group col-sm-8">
                <input class="form-control input-sm" id="newReplyText" type="text" placeholder="댓글 입력...">
              </div>
              <div class="form-group col-sm-2" hidden>
                <input class="form-control input-sm" id="newReplyWriter" type="text" value="${login.userId}" readonly>
              </div>
              <div class="form-group col-sm-2">
                <button type="button" class="btn btn-primary btn-sm btn-block replyAddBtn"> 
                	<i class="fa fa-save"></i> 저장
              	</button>
              </div>
            </div>
          </form>
        </c:if>
        <c:if test="${empty login}">
          <a href="${path}/user/login" class="btn btn-default btn-block" role="button">
          	<i class="fa fa-edit"></i> 로그인 한 사용자만 댓글 등록이 가능합니다.
          </a>
        </c:if>
        </div>
    </div>

    이제 댓글 목록 영역으로가서 SCRIPT 코드와 HMTL 코드를 추가하고 수정해주겠습니다. 

    Handlebars.registerHelper("eqReplyWriter", function (reply_writer, block) {
        var accum = "";
        if (reply_writer === "${login.userId}") {
            accum += block.fn();
        }
        return accum;
    });
    <script id="replyTemplate" type="text/x-handlebars-template">
        {{#each.}}
        <div class="post replyDiv" data-reply_no={{reply_no}}>
            <div class="user-block">
                <img class="img-circle img-bordered-sm" src="${path}/dist/img/user1-128x128.jpg" alt="user image">
                <span class="username">
                    <a href="#">{{reply_writer}}</a>
    				{{#eqReplyWriter reply_writer}}
                    <a href="#" class="float-right btn-box-tool replyDelBtn" data-toggle="modal" data-target="#delModal">
                        <i class="fa fa-times"> 삭제</i>
                    </a>
                    <a href="#" class="float-right btn-box-tool replyModBtn" data-toggle="modal" data-target="#modModal">
                        <i class="fa fa-edit"> 수정</i>
                    </a>
     				{{/eqReplyWriter}}
                </span>
                <span class="description">{{prettifyDate reg_date}}</span>
            </div>
            <div class="oldReplyText">{{reply_text}}</div>
            <br/>
        </div>
        {{/each}}
    </script>

     

    이제 비로그인 시 아래와 같이 댓글 수정과 삭제 버튼이 보이지 않게 처리된 것을 확인할 수 있습니다. 

     

     

     

    3-7. 게시글 작성자의 프로필 이미지 수정을 위한 준비

     댓글 작성자의 프로필 이미지가 각각의 회원에 프로필 이미지에 맞게 출력하기위한 작업을 수행해야하는데 가장먼저 replyMapper.xml을 수정해주겠습니다.

     

     replyMapper.xml에서 ReplyResultMap에 User ResultMap을 추가해주겠습니다. 아래와 같이 회원 이미지를 가져오기 위해서 association 태그를 사용함으로서 User ResultMap을 사용할 수 있게됩니다. 그리고 댓글 목록을 가져오는 sql문 또한 아래와 같이 수정함으로써 댓글 작성자의 프로필 이미지를 가져올 수 있도록 JOIN을 사용하겠습니다. 

    <?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="com.cameldev.mypage.mappers.reply.ReplyMapper">
    	 
         . . . 
         
         . . .
         
        <select id="listPaging" resultMap="ReplyResultMap">
    	    SELECT
    	        reply_no
    	        , article_no
    	        , reply_text
    	        , reply_writer
    	        , reg_date
    	        , update_date
    	        , user_img
    	    FROM tb_reply
    	    INNER JOIN tb_user ON user_id = reply_writer
    	    WHERE article_no = #{article_no}
    	    ORDER BY reply_no DESC
    	    LIMIT #{criteria.pageStart}, #{criteria.perPageNum}
    	</select>
    
        <resultMap id="ReplyResultMap" type="ReplyVO">
            <id property="reply_no" column="reply_no"/>
            <result property="article_no" column="article_no"/>
            <result property="reply_text" column="reply_text"/>
            <result property="reply_writer" column="reply_writer"/>
            <result property="reg_date" column="reg_date"/>
            <result property="update_date" column="update_date"/>
            <association property="userVO" resultMap="userVOResultMap"/>
        </resultMap>
    
        <resultMap id="ArticleResultMap" type="ArticleVO">
            <id property="article_no" column="article_no"/>
            <result property="title" column="title"/>
            <result property="content" column="content"/>
            <result property="writer" column="writer"/>
            <result property="regDate" column="regdate"/>
            <result property="viewCnt" column="viewcnt"/>
        </resultMap>
       
    	<resultMap id="userVOResultMap" type="UserVO">
            <id property="userId" column="user_id"/>
            <result property="userPw" column="user_pw"/>
            <result property="userName" column="user_name"/>
            <result property="userEmail" column="user_email"/>
            <result property="userJoinDate" column="user_join_date"/>
            <result property="userLoginDate" column="user_login_date"/>
            <result property="userSignature" column="user_signature"/>
            <result property="userImg" column="user_img"/>
            <result property="userPoint" column="user_point"/>
        </resultMap>
        
    </mapper>

    replyMapper.xml을 위와 같이 수정해주었다면 이제 ReplyVO에도 SQL문으로 가져온 정보를 처리할 수 있도록 UserVO를 멤버변수로 선언해주겠습니다. 

     

    하지만 아직 프로필 이미지 업로드 및 수정에 관련된 기능을 구현하지 않았기 때문에 정상적으로 처리되는지를 확인 할 수 없다는 점 참고하시길 바랍니다. 프로필 이미지 업로드 및 수정에 관된 내용은 추후에 포스팅하도록 하겠습니다. 

     

     

    4. 포스팅을 마치며

    이번 포스팅에서는 회원 가입과 로그인 처리에 대한 내용을 다뤄보았습니다. 다음 포스팅에서는 로그아웃 및 자동로그인 기능을 구현하겠습니다. 

     

     

    다음포스팅

    2020/07/04 - [Spring/게시판 만들기] - [Camel][Spring] 게시판 만들기 #10. 로그아웃, 로그인 유지 기능 구현

     

    [Camel][Spring] 게시판 만들기 #10. 로그아웃, 로그인 유지 기능 구현

    [Camel][Spring] 게시판 만들기 #10. 로그아웃, 로그인 유지 기능 구현 본 게시판 만들기 프로젝트는 더블에스 Devlog Spring-MVC 를 참조하여 작성했음을 알려드립니다. 또한 개인적인 학습을 목적

    cameldev.tistory.com

     

    글을 읽으시면서 잘못된 부분이나 궁금하신 사항은 댓글 달아주시면 빠른 시일내에 수정 및 답변하도록 하겠습니다. 

    댓글

Camel`s Tistory.