본문 바로가기

Server Programming/Spring Boot Backend Programming

3장. 세션과 필터, 쿠키와 리스너 (+ 한글 깨짐 처리 / Optional<> / 옵저버 패턴)

반응형

1. 세션과 필터

-과거의 상태를 유지하지 않는 무상태 연결인 단점을 해소하기 위해 사용하는 세션과 쿠키 그리고 문자열을 이용하는 토큰

 

2. 사용자 정의 쿠키

 

3. 리스너


Todo 애플리케이션 로그인 체크

-로그인 사용자만 Todo 등록 가능하도록 변경

 

1. TodoRegisterController의 doGet()메서드 오버라이딩

-쿠키를 확인하고, 로그인 여부 확인

(1) 쿠키가 없는 새로운 사용자 : isNew()로 확인

(2) 쿠키가 있지만 로그인 정보가 없는 사용자 : session.getAttribute("loginInfo") ==null로 확인

(3) 쿠키도 있고, 로그인 정보도 있는 사용자 : 등록 페이지로 이동

-> req.getRequestDispatcher("/WEB-INF/todo/register.jsp").forward(req,resp) 호출

 

package org.zerock.w2.controller;

import lombok.extern.log4j.Log4j2;
import org.zerock.w2.dto.TodoDTO;
import org.zerock.w2.service.TodoService;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;

@WebServlet(name = "todoRegisterController", value = "/todo/register")
@Log4j2
public class TodoRegisterController extends HttpServlet {

    private TodoService todoService = TodoService.INSTANCE;
    private final DateTimeFormatter DATEFORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        log.info("/todo/register GET .......");


        HttpSession session = req.getSession();

        if(session.isNew()) { //기존에 JSESSIONID가 없는 새로운 사용자
            log.info("JSESSIONID 쿠키가 새로 만들어진 사용자");
            resp.sendRedirect("/login");
            return;
        }

        //JSESSIONID는 있지만 해당 세션 컨텍스트에 loginInfo라는 이름으로 저장된
        //객체가 없는 경우
        if(session.getAttribute("loginInfo") == null) {
            log.info("로그인한 정보가 없는 사용자.");
            resp.sendRedirect("/login");
            return;
        }

        //정상적인 경우라면 입력 화면으로
        req.getRequestDispatcher("/WEB-INF/todo/register.jsp").forward(req,resp);

    }

}

 

2. 로그인 처리 컨트롤러 작성

-GET 방식으로 로그인 화면 출력

-POST 방식으로 실제 로그인 처리

 

(1) LoginController에 doGet 메서드 작성

package org.zerock.w2.controller;


import lombok.extern.log4j.Log4j2;
import org.zerock.w2.dto.MemberDTO;
import org.zerock.w2.service.MemberService;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.*;
import java.io.IOException;
import java.util.UUID;

@WebServlet("/login")
@Log4j2
public class LoginController extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        log.info("login get.............");

        req.getRequestDispatcher("/WEB-INF/login.jsp").forward(req,resp);
    }

}

-> 로그인 컨트롤러에서는 로그인 페이지로의 매핑 

 

(2) login.jsp 작성

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<html>
<head>
    <title>Title</title>
</head>
<body>

<form action="/login" method="post">
    <input type="text" name="mid">
    <input type="text" name="mpw">
    <button type="submit">LOGIN</button>
</form>
</body>
</html>

-> 입력 폼을 이용해 POST 방식으로 로그인 데이터 처리

 

(3) LoginController에서 doPost() 메서드 오버라이딩

-아이디와 비밀번호는 본디 DTO로 처리해야하지만, 먼저 문자열로 처리

-HttpSession을 이용해 얻은 세션에, 로그인 정보이름으로 문자열 보관

-로그인 정보 전달 후, 목록 화면으로 이동

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        log.info("login post........");

        String mid = req.getParameter("mid");
        String mpw = req.getParameter("mpw");

        String str = mid+mpw;

        HttpSession session = req.getSession();

        session.setAttribute("loginInfo", str);

        resp.sendRedirect("/todo/list");

    }

-> 로그인 정보가 존재하므로 이제 register 메서드 호출시 작성화면으로 이동

 

3. 동일한 로직의 중복 코드 방지를 위해 로그인 체크를 필터로 처리

 

(1) LoginCheckFilter 작성

  1. javax.servlet의 Filter 인터페이스를 구현해, doFilter 추상 클래스로 필터링 로직 구현
  2. @WebFilter 어노테이션으로 특정 urlPattern에 doFilter() 실행 
  3. 필터링 이후에는 다음 필터나 목적지로 이동하도록 FilterChain의 doFilter 호출
@WebFilter(urlPatterns = {"/todo/*"})
@Log4j2
public class LoginCheckFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {

        log.info("Login check filter....");

        chain.doFilter(request, response);
    }

추가해야할 기능

  1. 로그인 여부를 체크해, 로그인 되지 않았을 경우 에러 처리
  2. 리다이렉션을 통한 에러 처리 구현

 

(2) LoginCheckFilter에 로그인 여부 체크 

-javax.servlet.Filter 인터페이스의 doFilter()메서드는 HttpServletRequest/HttpServletResponse보다 상위 파라미터인 ServletRequest/ServletResponse를 사용

-따라서, HttpServletRequest/HttpServletResponse로 다운 캐스팅해서 세션을 얻어, 세션에 해당하는 올바른 로그인 정보가 존재하지 않으면, 응답을 리다이렉션처리

 

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {

        log.info("Login check filter....");

        HttpServletRequest req = (HttpServletRequest)request;
        HttpServletResponse resp = (HttpServletResponse)response;

        HttpSession session = req.getSession();

        if(session.getAttribute("loginInfo") == null){

            resp.sendRedirect("/login");

            return;
        }

        chain.doFilter(request, response);
    }

 

(3) 한글 깨짐 처리를 위해 UTF-8 처리 필터 적용

-HttpServletRequest의 한글 데이터를 UTF-8 캐릭터셋 적용 setCharacterEncoding("UTF-8")

-모든 경로에 적용하기 위해, urlPatterns={"/*"}

package org.zerock.w2.filter;

import lombok.extern.log4j.Log4j2;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

@WebFilter(urlPatterns = {"/*"})
@Log4j2
public class UTF8Filter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {

        log.info("UTF8  filter....");

        HttpServletRequest req = (HttpServletRequest)request;

        req.setCharacterEncoding("UTF-8");


        chain.doFilter(request, response);
    }
}

 

4. 세션을 이용해 로그아웃 처리

(1) 로그아웃 처리를 위해 LogoutController 작성

-HttpSession을 이용한 로그인을 처리했을 때, 가능한 2가지 로그아웃 방식

  1. 사용했던 정보를 삭제하는 방식
  2. 현재 HttpSession의 무효처리 - invalidate()
package org.zerock.w2.controller;

import lombok.extern.log4j.Log4j2;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;

@WebServlet("/logout")
@Log4j2
public class LogoutController extends HttpServlet {

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        log.info("log out..................");

        HttpSession session = req.getSession();

        session.removeAttribute("loginInfo");
        session.invalidate();

        resp.sendRedirect("/");

    }
}

-세션에서 로그인 정보를 지우고, 세션의 무효처리

-로그인 정보를 정보를 노출시키면 안되므로 POST 방식을 사용한다.

-로그아웃 이후에는 목록화면으로 리다이렉션 처리

 

(2) list.jsp에 로그아웃 실행하도록 <form> 태그 작성

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>

<html>
<head>
    <title>Todo List</title>
</head>
<body>
<h1>Todo List</h1>

<h2>${appName}</h2>
<h2>${loginInfo}</h2>
<h3>${loginInfo.mname}</h3>


<ul>
    <c:forEach items="${dtoList}" var="dto">
        <li>
            <span><a href="/todo/read?tno=${dto.tno}">${dto.tno}</a></span>
            <span>${dto.title}</span>
            <span>${dto.dueDate}</span>
            <span>${dto.finished? "DONE": "NOT YET"}</span>
        </li>
    </c:forEach>
</ul>

<form action="/logout" method="post">
    <button>LOGOUT</button>
</form>

</body>
</html>

 

5.데이터베이스에서 회원 정보 이용하기

-tbl_member 테이블은 본디 쿠키를 이용하지만, 현재는 최소한의 정보를 저장하도록 생성하고 나중에 변경한다.

 

(1) tbl_member 테이블 생성

create table tbl_member(
    mid varchar(50) primary key ,
    mpw varchar(50) not null ,
    mname varchar(100) not null
);

(2) 사용할 사용자 계정 insert 처리

insert into tbl_member (mid, mpw, mname) values ('user00','1111', '사용자0');
insert into tbl_member (mid, mpw, mname) values ('user01','1111', '사용자1');
insert into tbl_member (mid, mpw, mname) values ('user02','1111', '사용자2');

 

6. DB의 엔티티를 자바에서 객체로 처리

-객체로 처리하기 위해 VO/DAO 구현

 

(1) MemberVO와 MemberDAO 구현

-VO는 @Getter 어노테이션 적용 / DTO는 @Data 어노테이션 적용

package org.zerock.w2.domain;

import lombok.*;

@Getter
@ToString
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class MemberVO {

    private String mid;
    private String mpw;
    private String mname;
}

 

package org.zerock.w2.dao;

import lombok.Cleanup;
import org.zerock.w2.domain.MemberVO;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;

public class MemberDAO {


    public MemberVO getWithPassword(String mid, String mpw) throws Exception {

        String query = "select mid, mpw, mname from tbl_member where mid =? and mpw = ?";

        MemberVO memberVO = null;

        @Cleanup Connection connection = ConnectionUtil.INSTANCE.getConnection();
        @Cleanup PreparedStatement preparedStatement =
                connection.prepareStatement(query);
        preparedStatement.setString(1, mid);
        preparedStatement.setString(2, mpw);

        @Cleanup ResultSet resultSet = preparedStatement.executeQuery();

        resultSet.next();

        memberVO = MemberVO.builder()
                .mid(resultSet.getString(1))
                .mpw(resultSet.getString(2))
                .mname(resultSet.getString(3))
                .build();

        return memberVO;
    }

}

 

(2) MemberDTO와 MemberService 구현

-서비스 계층과 컨트롤러를 연결해주는 MemberDTO 작성

-DTO는 @Data 어노테이션 적용 / VO는 @Getter 어노테이션 적용

 

package org.zerock.w2.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class MemberDTO {

    private String mid;
    private String mpw;
    private String mname;
}

 

package org.zerock.w2.service;

import lombok.extern.log4j.Log4j2;
import org.modelmapper.ModelMapper;
import org.zerock.w2.dao.MemberDAO;
import org.zerock.w2.domain.MemberVO;
import org.zerock.w2.dto.MemberDTO;
import org.zerock.w2.util.MapperUtil;

@Log4j2
public enum MemberService {
    INSTANCE;

    private MemberDAO dao;
    private ModelMapper modelMapper;

    MemberService() {

        dao = new MemberDAO();
        modelMapper = MapperUtil.INSTANCE.get();

    }

}

-여러 곳에서 동일한 객체사용하도록 enum으로 구성하고 DAO 이용하도록 구성

 

(3) 로그인 처리를 위해 MemberService에 login() 메소드 작성

    public MemberDTO login(String mid, String mpw)throws Exception {

        MemberVO vo = dao.getWithPassword(mid, mpw);

        MemberDTO memberDTO = modelMapper.map(vo, MemberDTO.class);

        return memberDTO;
    }

 

7.컨트롤러에서 로그인 연동

-컨트롤러와 서비스 연동

 

(1) LoginController에서 doPost() 메소드가 MemberService를 통해 로그인되도록 연동처리

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        log.info("login post........");

        String mid = req.getParameter("mid");
        String mpw = req.getParameter("mpw");


        try {
            MemberDTO memberDTO = MemberService.INSTANCE.login(mid, mpw);


            HttpSession session = req.getSession();

            session.setAttribute("loginInfo", memberDTO);

            resp.sendRedirect("/todo/list");

        } catch (Exception e) {
            resp.sendRedirect("/login?result=error");
        }
    }

-올바른 로그인 확인한 경우에 HttpSession을 이용해 'loginInfo'라는 이름으로 객체 저장

-예외 발생시 '/login'으로 이동하는데, 'result'파라미터를 전달해 예외 발생 처리

 

(2) login.jsp에 JSTL을 이용해 에러 발생 처리

-EL에서 제공하는 param 객체를 이용해 result 파라미터로 전달받은 값을 확인해 에러 처리

-예외 발생 메시지가 저장된 ${param.result} 이용

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<html>
<head>
    <title>Title</title>
</head>
<body>

<c:if test="${param.result == 'error'}">
    <h1>로그인 에러</h1>
</c:if>

<form action="/login" method="post">
    <input type="text" name="mid">
    <input type="text" name="mpw">
    <input type="checkbox" name="auto">
    <button type="submit">LOGIN</button>
</form>
</body>
</html>

 

(3) EL의 스코프를 이용해 로그인한 사용자 정보 출력

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>

<html>
<head>
    <title>Todo List</title>
</head>
<body>
<h1>Todo List</h1>

<h2>${appName}</h2>
<h2>${loginInfo}</h2>
<h3>${loginInfo.mname}</h3>


<ul>
    <c:forEach items="${dtoList}" var="dto">
        <li>
            <span><a href="/todo/read?tno=${dto.tno}">${dto.tno}</a></span>
            <span>${dto.title}</span>
            <span>${dto.dueDate}</span>
            <span>${dto.finished? "DONE": "NOT YET"}</span>
        </li>
    </c:forEach>
</ul>

<form action="/logout" method="post">
    <button>LOGOUT</button>
</form>

</body>
</html>

*추가해야할 기능 : 쿠키를 이용해 자동로그인 처리

 

8. 쿠키를 이용해 조회한 Todo 보관 처리

-목록에서 조회한 번호를 쿠키에 보관

 

쿠키에 보관하는 작동 방식

  • 전송받은 쿠키 존재 -> 해당 쿠키 값 활용 
    전송받은 쿠키 없음 -> 새로운 문자열 생성
  • 쿠키 이름 : viewTodos
  • 문자열 내에 현재 Todo 번호를 문자열로 연결하는데, 이미 조회한 번호는 추가하지 않는다.
  • 쿠키 유효시간은 24시간

 

(1) 쿠키를 적용하기 위해 TodoReadController 변경

-현재 요청에 존재하는 모든 쿠키 중 조회 목록 쿠키를 찾는 메소드 추가

-특정 tno가 쿠키에 있는지 확인하는 코드 추가

 

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        try {
            Long tno = Long.parseLong(req.getParameter("tno"));

            TodoDTO todoDTO = todoService.get(tno);

            //모델 담기
            req.setAttribute("dto", todoDTO);

            //쿠키 찾기
            Cookie viewTodoCookie = findCookie(req.getCookies(), "viewTodos");

            String todoListStr = viewTodoCookie.getValue();

            boolean exist = false;

            if(todoListStr != null && todoListStr.indexOf(tno+"-") >= 0){
                exist = true;
            }

            log.info("exist: " + exist);

            if(!exist) {
                todoListStr += tno+"-";
                viewTodoCookie.setValue(todoListStr);
                viewTodoCookie.setMaxAge(60* 60* 24);
                viewTodoCookie.setPath("/");
                resp.addCookie(viewTodoCookie);
            }



            req.getRequestDispatcher("/WEB-INF/todo/read.jsp").forward(req, resp);

        }catch(Exception e){
            e.printStackTrace();
            log.error(e.getMessage());
            throw new ServletException("read error");
        }
    }
    private Cookie findCookie(Cookie[] cookies, String cookieName) {

        Cookie targetCookie = null;

        if(cookies != null && cookies.length > 0){
            for (Cookie ck:cookies) {
                if(ck.getName().equals(cookieName)){
                    targetCookie = ck;
                    break;
                }
            }
        }

        if(targetCookie == null){
            targetCookie = new Cookie(cookieName, "");
            targetCookie.setPath("/");
            targetCookie.setMaxAge(60*60*24);
        }

        return targetCookie;
    }

 

 

(2) TodoReadController의 doGet() 메소드 변경

 

변경 전, TodoReadController

@WebServlet(name = "todoReadController", value = "/todo/read")
@Log4j2
public class TodoReadController extends HttpServlet {

    private TodoService todoService = TodoService.INSTANCE;

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        try {
            Long tno = Long.parseLong(req.getParameter("tno"));

            TodoDTO todoDTO = todoService.get(tno);

            //모델 담기
            req.setAttribute("dto", todoDTO);

            req.getRequestDispatcher("/WEB-INF/todo/read.jsp").forward(req, resp);

        }catch(Exception e){
            log.error(e.getMessage());
            throw new ServletException("read error");
        }
    }
}

 

변경 후, TodoReadController

-findCookie() 메서드 추가

-쿠키 내용물 검사

-쿠키 변경시, 경로나 유효시간 재설정

 

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        try {
            Long tno = Long.parseLong(req.getParameter("tno"));

            TodoDTO todoDTO = todoService.get(tno);

            //모델 담기
            req.setAttribute("dto", todoDTO);

            //쿠키 찾기
            Cookie viewTodoCookie = findCookie(req.getCookies(), "viewTodos");

            String todoListStr = viewTodoCookie.getValue();

            boolean exist = false;

            if(todoListStr != null && todoListStr.indexOf(tno+"-") >= 0){
                exist = true;
            }

            log.info("exist: " + exist);

            if(!exist) {
                todoListStr += tno+"-";
                viewTodoCookie.setValue(todoListStr);
                viewTodoCookie.setMaxAge(60* 60* 24);
                viewTodoCookie.setPath("/");
                resp.addCookie(viewTodoCookie);
            }



            req.getRequestDispatcher("/WEB-INF/todo/read.jsp").forward(req, resp);

        }catch(Exception e){
            e.printStackTrace();
            log.error(e.getMessage());
            throw new ServletException("read error");
        }
    }
    private Cookie findCookie(Cookie[] cookies, String cookieName) {

        Cookie targetCookie = null;

        if(cookies != null && cookies.length > 0){
            for (Cookie ck:cookies) {
                if(ck.getName().equals(cookieName)){
                    targetCookie = ck;
                    break;
                }
            }
        }

        if(targetCookie == null){
            targetCookie = new Cookie(cookieName, "");
            targetCookie.setPath("/");
            targetCookie.setMaxAge(60*60*24);
        }

        return targetCookie;
    }

 

9. 쿠키와 세션을 같이 활용해 자동 로그인 처리 구현

 

(1) tbl_member 테이블에 임의의 문자열을 보관하기 위한 uuid 칼럼 추가

alter table tbl_member add column uuid varchar(50);

 

(2) 자동 로그인 체크를 위한 'auto'라는 이름의 체크 박스 추가

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<html>
<head>
    <title>Title</title>
</head>
<body>

<c:if test="${param.result == 'error'}">
    <h1>로그인 에러</h1>
</c:if>

<form action="/login" method="post">
    <input type="text" name="mid">
    <input type="text" name="mpw">
    <input type="checkbox" name="auto">
    <button type="submit">LOGIN</button>
</form>
</body>
</html>

 

(3) LoginController의 doPost() 메서드에서 'auto' 체크박스 값 확인

-'on'일 경우 자동 로그인 처리

 

변경 전, doPost()

@Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        log.info("login post........");

        String mid = req.getParameter("mid");
        String mpw = req.getParameter("mpw");


        try {
            MemberDTO memberDTO = MemberService.INSTANCE.login(mid, mpw);


            HttpSession session = req.getSession();

            session.setAttribute("loginInfo", memberDTO);

            resp.sendRedirect("/todo/list");

        } catch (Exception e) {
            resp.sendRedirect("/login?result=error");
        }
    }

 

변경 후, doPost()

-auto값 가져와 변수를 만들어 저장하고, 해당 값이 'on'이면 boolean값에 true 저장

-참이면, UUID를 이용해 임의의 번호 저장

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        log.info("login post........");

        String mid = req.getParameter("mid");
        String mpw = req.getParameter("mpw");

        String auto  = req.getParameter("auto");

        boolean rememberMe = auto != null && auto.equals("on");

        log.info("-----------------------------");
        log.info(rememberMe);


        try {
            MemberDTO memberDTO = MemberService.INSTANCE.login(mid, mpw);

            if(rememberMe){
                String uuid = UUID.randomUUID().toString();
                }

       }
    }

 

(4) MemberVO, MemberDTO에 uuid 추가

(5) 자동로그인을 위한 MemberDAO 변경

- MemberDAO 자동로그인 설정시 tbl_member의 사용자 정보에 uuid 수정하는 updateUuid() 메소드 추가

    public void updateUuid(String mid, String uuid) throws  Exception {

        String sql = "update tbl_member set uuid =? where mid = ?";

        @Cleanup Connection connection = ConnectionUtil.INSTANCE.getConnection();
        @Cleanup PreparedStatement preparedStatement =
                connection.prepareStatement(sql);

        preparedStatement.setString(1, uuid);
        preparedStatement.setString(2, mid);


        preparedStatement.executeUpdate();

    }

(6) MemberService에 MemberDAO의 updateUuid() 메서드 추가

    public void updateUuid(String mid, String uuid)throws Exception {

        dao.updateUuid(mid, uuid);

    }

(7) LoginController의 로그인 후에 uuid업데이트 하도록, updateUuid() 추가

@Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        log.info("login post........");

        String mid = req.getParameter("mid");
        String mpw = req.getParameter("mpw");

        String auto  = req.getParameter("auto");

        boolean rememberMe = auto != null && auto.equals("on");

        log.info("-----------------------------");
        log.info(rememberMe);


        try {
            MemberDTO memberDTO = MemberService.INSTANCE.login(mid, mpw);

            if(rememberMe){
                String uuid = UUID.randomUUID().toString();

                MemberService.INSTANCE.updateUuid(mid, uuid);
                memberDTO.setUuid(uuid);

            }


            HttpSession session = req.getSession();

            session.setAttribute("loginInfo", memberDTO);

            resp.sendRedirect("/todo/list");

        } catch (Exception e) {
            resp.sendRedirect("/login?result=error");
        }
    }

-> 로그인시에 체크박스 체크시 데이터베이스에 임의의 값이 생성된다.

 

(8)쿠키 생성 및 전송

-쿠키에 문자열이 제대로 처리되었으면, LoginController에서 브라우저에 remember-me라는 쿠키를 생성해 전송

    if(rememberMe){
                String uuid = UUID.randomUUID().toString();

                MemberService.INSTANCE.updateUuid(mid, uuid);
                memberDTO.setUuid(uuid);

                Cookie rememberCookie =
                        new Cookie("remember-me", uuid);
                rememberCookie.setMaxAge(60*60*24*7);  //쿠키의 유효기간은 1주일
                rememberCookie.setPath("/");

                resp.addCookie(rememberCookie);

            }

(9) 쿠키 값을 이용한 사용자 조회

-쿠키 안의 UUID 값을 이용해 해당 사용자 정보를 가져오기 위해, MemberDAO()에 selectUUID()메서드 추가

-MemberService()에서 MemberDAO()의 selectUUID()메서드 호출해 UUID값을 이용해 사용자를 찾는 메서드 getByUUID() 추가

 

    public MemberVO selectUUID(String uuid) throws Exception{

        String query = "select mid, mpw, mname, uuid from tbl_member where uuid =?";

        @Cleanup Connection connection = ConnectionUtil.INSTANCE.getConnection();
        @Cleanup PreparedStatement preparedStatement =
                connection.prepareStatement(query);
        preparedStatement.setString(1, uuid);

        @Cleanup ResultSet resultSet = preparedStatement.executeQuery();

        resultSet.next();

        MemberVO memberVO = MemberVO.builder()
                .mid(resultSet.getString(1))
                .mpw(resultSet.getString(2))
                .mname(resultSet.getString(3))
                .uuid(resultSet.getString(4))
                .build();

        return memberVO;

    }

 

    public MemberDTO getByUUID(String uuid) throws  Exception {

        MemberVO vo = dao.selectUUID(uuid);

        MemberDTO memberDTO = modelMapper.map(vo, MemberDTO.class);

        return memberDTO;
    }

 

(10) LoginCheckFilter에서 쿠키 체크

-현재 구현된 사항 : LoginCheckFilter에서 HttpSession에 'loginInfo'라는 이름의 객체 저장여부만 확인

-변경할 사항 : HttpSession에 없고, 쿠키에 UUID 값만 있는지 확인

 

로그인 체크 동작 순서

  1. HttpServletRequest를 이용해 모든 쿠키 중에서 'remember-me' 이름의 쿠키를 검색
  2. 해당 쿠키 값으로 MemberService를 통해 MemberDTO 구성
  3. HttpSession으로 'loginInfo'라는 이름의 객체에 MemberDTO를 setAttribute()
  4. 정상적으로 FilterChain의 doFilter()를 수행
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {

        log.info("Login check filter....");

        HttpServletRequest req = (HttpServletRequest)request;
        HttpServletResponse resp = (HttpServletResponse)response;

        HttpSession session = req.getSession();

        if(session.getAttribute("loginInfo") == null){

            //쿠키를 체크
            Cookie cookie = findCookie(req.getCookies(), "remember-me");

            if(cookie != null){

                log.info("cookie는 존재하는 상황");
                String uuid  = cookie.getValue();

                try {
                    MemberDTO memberDTO = MemberService.INSTANCE.getByUUID(uuid);

                    log.info("쿠키의 값으로 조회한 사용자 정보: " + memberDTO );

                    session.setAttribute("loginInfo", memberDTO);
                } catch (Exception e) {
                    e.printStackTrace();
                }

                chain.doFilter(request, response);
                return;
            }

            resp.sendRedirect("/login");

            return;
        }

        chain.doFilter(request, response);
    }

    private Cookie findCookie(Cookie[] cookies, String name){

        if(cookies == null || cookies.length == 0){
            return null;
        }

        Optional<Cookie> result = Arrays.stream(cookies).filter(ck -> ck.getName().equals(name)).findFirst();

        return result.isPresent()?result.get():null;
    }

 

자동 로그인과 로그인정보를 이용해 발생하는 조건 분기를 통해 두 가지 경우의 수 처리

  1. 로그인 정보도 없고, 자동로그인 처리도 되어있지 않을 경우
    -HttpSession 내에 loginInfo라는 이름의 객체도 존재하지 않고, remember-me 쿠키도 없는 경우에 로그인 페이지로 리다이렉션
  2. 세션 정보는 없지만, 쿠키는 존재할 경우
    -DB에서 UUID 존재한다면, 쿠키 전송여부에 따라 로그인 처리

 

10. 프로젝트 실행/종료시 특정 작업 수행 적용

 

(1) W2AppListener 코드 수정

-ServletContextEvent에서 ServletContext 객체를 찾아서, 해당 객체에 setAttribute()로 원하는 이름으로 객체 보관

package org.zerock.w2.listener;


import lombok.extern.log4j.Log4j2;

import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;

@WebListener
@Log4j2
public class W2AppListener implements ServletContextListener {

    @Override
    public void contextInitialized(ServletContextEvent sce) {

        log.info("----------init---------------------------");
        log.info("----------init---------------------------");
        log.info("----------init---------------------------");

        ServletContext servletContext = sce.getServletContext();

        servletContext.setAttribute("appName", "W2");

    }

    @Override
    public void contextDestroyed(ServletContextEvent sce) {

        log.info("----------destroy---------------------------");
        log.info("----------destroy---------------------------");
        log.info("----------destroy---------------------------");

    }
}

 

(2) TodoListController에서 HttpServletRequest의 getServletContext() 메소드 활용

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        log.info("todo list..................");

        ServletContext servletContext = req.getServletContext();

        log.info("appName:  "  + servletContext.getAttribute("appName"));


        try {
            List<TodoDTO> dtoList = todoService.listAll();
            req.setAttribute("dtoList", dtoList);
            req.getRequestDispatcher("/WEB-INF/todo/list.jsp").forward(req,resp);
        } catch (Exception e) {
            log.error(e.getMessage());
            throw new ServletException("list error");
        }
    }

 

(3) 저장한 리스트를 파라미터로 전달한 list.jsp에 추가

<h1>Todo List</h1>

<h2>${appName}</h2>
<h2>${loginInfo}</h2>
<h3>${loginInfo.mname}</h3>

 

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>

<html>
<head>
    <title>Todo List</title>
</head>
<body>
<h1>Todo List</h1>

<h2>${appName}</h2>
<h2>${loginInfo}</h2>
<h3>${loginInfo.mname}</h3>


<ul>
    <c:forEach items="${dtoList}" var="dto">
        <li>
            <span><a href="/todo/read?tno=${dto.tno}">${dto.tno}</a></span>
            <span>${dto.title}</span>
            <span>${dto.dueDate}</span>
            <span>${dto.finished? "DONE": "NOT YET"}</span>
        </li>
    </c:forEach>
</ul>

<form action="/logout" method="post">
    <button>LOGOUT</button>
</form>

</body>
</html>

 

11. 세션 작업 감시하는 리스너 등록

-HttpSession 관련 작업을 감시하는 리스너 (HttpSessionListener / HttpSessionAttributeListener)를 이용해 이를 감지한다.

-HttpSession 생성과 setAttribute()작업 수행시 감지하는 LoginListener 클래스 작성

package org.zerock.w2.listener;

import lombok.extern.log4j.Log4j2;

import javax.servlet.annotation.WebListener;
import javax.servlet.http.HttpSessionAttributeListener;
import javax.servlet.http.HttpSessionBindingEvent;

@WebListener
@Log4j2
public class LoginListener implements HttpSessionAttributeListener {

    @Override
    public void attributeAdded(HttpSessionBindingEvent event) {

        String name = event.getName();

        Object obj = event.getValue();

        if(name.equals("loginInfo")){
            log.info("A user logined...........");
            log.info(obj);
        }
    }
}

-> HttpSessionAttributeListener 인터페이스를 구현해 attributeAdded(), attributeRemoved(), attributeReplacedI()를 이용해 HttpSession에 setAttribute()/removeAttribute() 작업 감지

 


세션과 필터

무상태에서 과거를 기억하는 법

-쿠키를 이용해 과거의 방문 기록을 추적하는 세션 트랙킹

 

쿠키

-문자열로 만들어진 데이터 조각으로 요청과 응답 시에 주고 받는다.

-이름-값의 구조로 여러 개의 쿠키가 사용되기도 한다.

 

쿠키를 주고받는 과정

  • 최초로 서버 호출시엔 아무것도 전송하지 않는다.
  • 서버에서 응답메시지 보낼 때 Set-Cookie라는 HTTP 헤더를 이용해 쿠키 전송 
  • 받은 쿠키의 유효기간(만료기간)을 보고 보관 방법 결정
  • 서버에 요청시 HTTP 요청할 때 HTTP 헤더에 경로를 맞는 Cookie라는 헤더 이름과 함께 전달
  • 서버에서는 필요에 따라 브라우저가 보낸 쿠키를 사용한다.

쿠키 생성하는 두 가지 방법

  • 서버에서 자동으로 생성하는 쿠키 : 응답 메시지 작성시 기존의 쿠키가 없으면 WAS에서 자동 발행
    *WAS마다 고유 이름이 다른데, 톰캣의 경우 JSESSIONID라는 이름을 이용해 쿠키 생성하며 브라우저 메모리상에 저장한다.
  • 개발자가 생성하는 쿠키 : 이름, 유효기간, 경로, 도메인을 지정가능하며, 반드시 직접 응답에 추가해야한다.

서블릿 컨텍스트와 세션 저장소

서버에서 생성하는 쿠키를 이해하기 위한 개념

 

서블릿 컨텍스트

-톰캣 하나가 여러 별도의 도메인으로 분리해 웹 애플리케이션을 운영하고, 각각의 웹 애플리케이션의 고유한 메모리에 인스턴스로 만들어 서비스를 제공하는데 그 메모리 영역을 서블릿 컨텍스트라고 말한다.

 

세션 저장소

-WAS에서 생성한 쿠키인 세션 쿠키를 관리하는 메모리 영역을 세션 저장소라고 말한다.

-'키'-'값'을 보관하는 구조 -> 'JSESSIONID' - 쿠키 값

-톰캣이 주기적으로 사용하지 않는 쿠키를 제거한다.

 

세션을 통한 상태 유지 메커니즘

HttpServletRequest의 getSession() 메소드 실행시 JSESSIONID라는 이름의 쿠키 존재여부 확인

-> 없으면 새로운 값을 만들어 세션 저장소에 보관하고 쿠키 값에 따라 JSESSION의 공간에 원하는 객체를 보관한다.

 

HttpServeltRequest의 getSession() 메소드

  • JSESSIONID 존재하지 않으면, 세션 저장소에 새로운 번호로 공간 만들어 접근가능한 객체 반환하고 해당 번호 전송
  • JSESSIONID가 존재하면, 세션 저장소에서 해당 ID 값의 공간에 접근 가능한 객체를 반환

->리턴타입은 세션 컨텍스트라고 부르는 HttpSession으로 해당 타입 객체를 이용해 저장, 수정/삭제 가능

->isNew() 메소드로 새로운 공간여부 구분 가능

 


세션을 이용하는 로그인 체크

  1. 로그인 성공시 HttpSession을 이용해 해당 세션 컨텍스트에 객체를 이름과 함께 저장
  2. 로그인 체크가 필요한 컨트롤러에서 세션 컨텍스트에 지정된 키로 객체 저장 여부 확인
    -> 존재시 로그인된 사용자로 간주 / 존재하지 않으면 로그인 페이지로 이동

 

필터를 이용한 로그인 체크

-로그인 여부 체크가 필요한 컨트롤러마다 동일 로직을 작성한다면, 중복된 코드를 작성해야 하므로

중복 코드의 방지를 위해 필터를 이용해 처리

-필터 : 서블릿이나 JSP에 요청을 전달하기 전에 필터링하는 역할을 하는 객체

-@WebFilter 어노테이션을 이용해 특정 경로에 대해 동일한 필터를 동작하도록 처리

-> 동일한 로직을 필터로 처리하므로, 중복 코드의 작성을 방지할 수 있다.

 

세션을 이용하는 로그아웃 처리

 

데이터베이스에서 회원 정보 이용하기

EL의 스코프와 스코프를 이용한 HttpSession 접근 : 변수의 범위의 개념과 유사한 EL의 스코프

-EL에서 기본 제공하는 param 객체. ${param.result}를 이용해 접근 가능.

-EL을 이용해 HttpServletRequest에 setAttribute()로 저장된 객체를 사용할 수 있는데,  HttpServletRequest에서 저장된 객체를 찾을 수 없을 경우, HttpSession에서 객체를 찾는다.

-HttpServletRequest나 HttpSession에서 setAttribute()로 되어 있는 데이터 찾을 때 사용한다.

 

스코프를 이용해 순서대로 접근하는 변수

  1. Page Scope
  2. Request Scope
  3. Session Scope
  4. Application Scope

 

 

 

 


사용자 정의 쿠키

WAS가 생성한 세션쿠키를 이용하는 HttpSession, 개발자가 사용하기 위해 생성되는 사용자 정의 쿠키 

쿠키는 서버와 브라우저를 오고간다는 장점이 있지만, 보안성이 떨어진다는 단점으로 인해 편의성을 제공하는 부분에서 제한적으로 사용

'오늘 하루 이창 열지 않기' / '최근 본 상품 목록' / '자동 로그인' / '조회수'

 

-> 쿠키의 떨어지는 보안성을 보완하기 위해 차후에 주기적으로 쿠키 값을 갱신하도록 설정해야 한다.

 

쿠키의 생성/전송 :newCookie()/ addCookie()

  사용자 정의 쿠키 WAS에서 발행하는 쿠키(세션 쿠키)
생성 개발자가 직접 newCookie( )로 생성
경로도 지정 가능
자동
전송 반드시 HttpServletResponse에 addCookie()를 통해야만 전송  
유효기간 쿠키 생성시에 초단위로 지정할 수 있음 지정불가
브라우저의 보관방식 유효기간이 없는 경우에는 메모리상에만 보관
유효기간이 있는 경우에는 파일이나 기타 방식으로 보관
메모리상에만 보관
쿠키의 크기 4kb 4kb

-> 사용자 정의 쿠키는 문자열로 된 이름-값으로 저장되는데, 값의 경우 URLEncoding된 문자열이 필요하다.

 

쿠키와 세션을 같이 활용하기

쿠키와 세션을 이용해 간단히 검증하는 수준으로 자동로그인 구현

*차후에 스프링 부트와 시큐리티를 이용해 자동로그인 처리를 개선해야 한다.

 

자동 로그인 작동 방식

  1. 로그인한 사용자 정보를 쿠키에 보관
  2. 보관한 쿠키를 이용해 사용자 정보를 HttpSession에 담는다.

 

자동 로그인 준비

로그인 구현

  • 사용자 로그인시, 임의의 문자열 생성하고 DB에 보관
  • 쿠키에 생성된 문자열을 값으로 하고, 유효기간은 1주일

로그인 체크 구현

  • 현재 사용자의 HttpSession에 로그인 정보 없을 때 쿠키 확인
  • 쿠키 값과 DB 값 비교해 같으면, 사용자 정보를 가져와 HttpSession에 사용자 정보 추가

쿠키 값을 주기적으로 갱신하는 개선사항을 적용하기 전에, UUID(Universally Unique Identifier)를 이용해 임의의 문자열 처리

->UUID :  java.util 패키지를 이용해 범용 고유 식별자로 고유한 번호를 랜덤 생성

 

 

*차후에 개선해야할 사항 : 쿠키 값 탈취시 발생하는 보안 문제를 방지하기 위해 쿠키값의 주기적 갱신 기능 추가


리스너

리스너는 이벤트라는 특정한 데이터 발생시 자동으로 실행되어, 특정 이벤트 발생시 설정해둔 매크로 수행함으로써 기존 코드의 변경없이 추가 기능 수행

-> 리스너를 통해 동작하는 스프링 MVC

 

리스너 개념과 용도

어떤 작업 발생시 작업 목록 수행

-> '현재 서버에 접속한 모든 사용자의 IP를 로그에 남긴다.'

 

옵저버 패턴

-특정한 변화를 구독(subscribe)하는 객체들을 보관한 상태에서 변화(publish) 발생시 구독 객체 실행

-'재난 감시 시스템'

-센서가 데이터 감지 이벤트 발생해 관제 센터에 통보 -> 관제 센터에서 산하 기관 (이벤트 리스너)에 이벤트 발생 통보

 

 

이미 구현되어 있는 리스너 인터페이스를 이용

  1. 해당 웹 애플리케이션의 시작 종료시 특정 작업 수행
  2. HttpSession에 특정 작업 감시와 처리
  3. HttpServletRequest에 특정 작업 감시와 처리

 

해당 프로젝트 실행/종료시 특정 작업 수행하기 위한 ServletContextListener

-ServletContextListener를 구현하는 W2AppListener 클래스 작성

-@WebListener 어노테이션 추가하고, contextInitalized()와 contextDestroyed() 메서드 오버라이딩

-프로젝트를 실행 / 종료시 로그출력

 

package org.zerock.w2.listener;


import lombok.extern.log4j.Log4j2;

import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;

@WebListener
@Log4j2
public class W2AppListener implements ServletContextListener {

    @Override
    public void contextInitialized(ServletContextEvent sce) {

        log.info("----------init---------------------------");
        log.info("----------init---------------------------");
        log.info("----------init---------------------------");


    }

    @Override
    public void contextDestroyed(ServletContextEvent sce) {

        log.info("----------destroy---------------------------");
        log.info("----------destroy---------------------------");
        log.info("----------destroy---------------------------");

    }
}

 

 

ServletContextEvent와 ServletContext

-contextInialiszed() / contextDestroyed()의 파라미터로

현재 애플리케이션 실행 공간의 모든 자원들에 접근할 수 있는 ServletContextEvent객체가 전달

 

-ServletContext에서 setAttribute()를 이용해 원하는 이름으로 객체 보관하고 저장된 객체를 공유해서 사용

#EL 표현식을 이용해 ${appName}으로 활용 가능

 

 

ServletContextListener와 스프링 프레임워크

ServletContextListener와 ServletContext를 이용해 프로젝트 실행시 필요한 객체 준비작업처리

-> 커넥션풀 초기화 / TodoService 객체를 생성해 보관

-> enum으로 객체 하나만 생성했던 모든 예제 처리 가능

 

#ServletContextListener는 스프링 프레임워크를 미리 로딩하는 작업 처리시 사용

 

반응형