본문 바로가기
Spring

서블릿 API 필수적 개념 (세션, 쿠키, 필터, 리스너)

by SuldenLion 2023. 9. 17.
반응형

▶ 세션과 필터

- 웹은 기본적으로 과거의 상태를 유지하지 않는 Stateless(무상태) 연결.

- 요청과 응답을 하나의 단위로 처리하면서 기존 사용자에 대한 정보는 기억하지 않음.

- Stateless라는 특징으로 인해 기존의 방문자를 기억하기 위해서 특별한 메커니즘을 사용하는데, 세션(HttpSession)이나 쿠키(Cookie) 또는 특정한 문자(Token)을 이용함.

↳ 로그인 유지를 위한 모든 기능을 웹에서는 세션 트랙킹(Session tracking)이라고 함.

 

 

▷ 쿠키(Cookie)

- 문자열로 만들어진 데이터의 조각으로 서버와 브라우저 사이에서 요청이나 응답시에 주고받는 형태로 사용.

- 쿠키는 문자열로 되어있는 정보로 가장 기본적인 형태는 이름(name)과 값(value)의 구조

- 개발자 도구의 application 메뉴에서 쿠키 확인 가능

 

 쿠키를 주고받는 시나리오

- 브라우저에서 최초로 서버를 호출하는 경우에 해당 서버에서 발행한 쿠키가 없다면 브라우저는 아무것도 전송하지 않음.

- 서버에서는 응답 메시지를 보낼때 브라우저에게 쿠키를 보내주는데 이때 Set-Cookie라는 HTTP 헤더 사용.

- 브라우저는 쿠키를 받은 후에 이에 대한 정보를 읽고, 이를 파일 형태로 보관할 것인지 메모리상에서만 처리할 것인지 결정함. (쿠키에 있는 유효기간을 보고 판단)

- 브라우저가 보관하는 쿠키는 다음에 다시 브라우저가 서버에 요청할 때 HTTP 헤더에 Cookie라는 헤더 이름과 함께 전달. (쿠키에는 경로를 지정할 수 있어서 해당 경로에 맞는 쿠키가 전송)

- 서버에서는 필요에 따라서 브라우저가 보낸 쿠키를 읽고 이를 사용.

 

⦁ 쿠키 생성 방법

- 서버에서 쿠키를 발행하는 것은 서버에서 자동으로 발행되는 방식개발자가 코드를 통해 직접 발행하는 두 가지 방식이 존재.

 

> 서버에서 자동으로 생성하는 쿠키 

- 응답 메시지를 작성할 때 정해진 쿠키가 없는 경우 자동으로 발행. WAS에서 발행되며 이름은 WAS마다 고유한 이름을 사용해서 쿠키 생성 (톰캣은 'JSESSIONID'라는 이름 사용)

- 서버에서 발행하는 쿠키는 기본적으로 브라우저의 메모리상에 보관. → 브라우저 종료시 서버에서 발행한 쿠키 삭제됨

- 서버에서 발행하는 쿠키의 경로는 '/'로 지정됨

 

> 개발자가 생성하는 쿠키

- 이름을 원하는대로 지정 가능

- 유효기간 지정 가능 (유효기간이 지정되면 브라우저가 이를 파일의 형태로 보관)

- 반드시 직접 응답(Response)에 추가해 주어야 함.

- 경로나 도메인 등을 지정할 수 있음. (특정한 서버의 경로를 호출하는 경우에만 쿠키를 사용)

 

 

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

- 쿠키 이해를 위해 필요한 개념. (서버는 톰캣이라고 가정)

- 하나의 톰캣은 여러개의 웹 애플리케이션(웹 프로젝트)을 실행할 수 있음. 

- 웹 애플리케이션마다 별도의 도메인으로 분리해서 운영됨.

- 각각의 웹 애플리케이션은 자신만이 사용하는 고유의 메모리 영역(= 서블릿 컨텍스트)을 하나 생성해서 이 공간에 서블릿이나 JSP 등을 인스턴스로 만들어 서비스를 제공함.

- 각각의 웹 애플리케이션을 생성할 때는 톰캣이 발행하는 쿠키(세션 쿠키)들을 관리하기 위한 메모리 영역이 하나 더 생성되는데 이 영역을 세션 저장소(Session repository)라고 함.

 

- 세션 저장소는 기본적으로 key-value를 보관하는 구조. (키의 역할을 하는 것이 톰캣에서 JSESSIONID라는 쿠키의 값이 됨)

- 톰캣 내부의 세션 저장소는 발행된 쿠키들의 정보를 보관하는 역할을 하게 되는데 문제는 새로운 JSESSIONID 쿠키가 만들어 질때마다 메모리 공간을 차지함. → 이 문제 해결을 위해 톰캣은 주기적으로 세션 저장소를 조사하면서 더 이상 사용하지 않는 값들을 정리하는 방식으로 동작함.

- 값 정리 방식은 session-timeout 설정 이용. 지정된 시간보다 오래된 값들은 주기적인 검사과정에서 삭제. (톰캣은 기본 30분)

 

 

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

- 코드상에서 HttpServletRequest의 getSession() 메서드를 실행하면 톰캣에서는 JSESSIONID 이름의 쿠키가 요청할 떄 있었는지 확인하고 없다면 새로운 값을 만들어 세션 저장소에서 보관함.

- 세션 저장소에서는 JSESSIONID의 값마다 고유한 공간을 가지게 되는데 이 공간은 다시 key와 value로 데이터 보관 가능

↳ ex) 3개의 브라우저가 처음으로 세션이 필요한 경로를 요청했다고 가정하고, JSESSIONID 값이 각각 'A1234', 'B111', 'C333'라고 가정. 이 공간들을 이용해서 서블릿/JSP 등은 원하는 객체들을 보관할 수 있는데, 사용자들마다 다른 객체들을 다음과 같은 형태로 보관.

'A1234'와 'B111'은 자신이 사용하는 공간에 login 정보가 존재하는데 서버에서 프로그램을 작성할때 이를 이용해서 해당 사용자가 로그인했다는 것을 인정하는 방식. getSession() 메서드로 각 공간에 접근.

 

 

HttpServletRequest의 getSession()

- JSESSIONID가 없는 경우의 작업 : 세션 저장소에 새로운 번호로 공간을 만들고 해당 공간에 접근할 수 있는 객체를 반환. 새로운 번호는 브라우저에 JSESSIONID의 값으로 전송 (세션 쿠키)

- JSESSIONID가 있는 경우의 작업 : 세션 저장소에서 JSESSIONID 값을 이용해서 할당된 공간을 찾고 이 공간에 접근할 수 있는 객체를 반환.

- getSession()의 결과물은 세션 저장소 내의 공간인데 이 공간을 의미하는 타입은 HttpSession 타입이며, 해당 공간은 세션 컨텍스트(Session Context) 혹은 세션(Session)이라 함.

 

 

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

- 사용자가 로그인에 성공하면 HttpSession을 이용해서 해당 사용자의 공간(세션 컨텍스트)에 특정한 객체를 이름과 함께 저장함.

- 로그인 체크가 필요한 컨트롤러에서는 현재 사용자의 공간에 지정된 이름으로 객체가 저장되어 있는지를 확인함. 만일 객체가 존재한다면 해당 사용자는 로그인된 사용자로 간주하고 그렇지 않다면 로그인 페이지로 이동시킴.

 

 

> 등록 시, 로그인 체크

- 로그인한 사용자만이 서비스를 이용할 수 있을 때의 RegisterController의 doGet()

@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);

}

 

- 코드에서 HttpServletRequest의 getSession()을 호출했기 때문에 새로운 값이 생성되어 브라우저로 전송되었고, 서버에서는 새로운 값을 Set-Cookie라는 헤더를 이용해서 저장.

 

 

> 로그인 처리 컨트롤러

- '/login'이라는 경로에서 GET 방식은 로그인 화면을 보여주고, POST 방식으로 실제 로그인을 처리하도록 구성.

// Log4j2, Servlet 관련 패키지들, IOException 임포트

@WebServlet("/login")
@Log
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);
    }
    
    @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");
    }
    
}

LoginController에서는 POST 방식으로 파라미터들을 수집하고, HttpSession에 'loginInfo' 이름을 이용해서 간단한 문자열을 저장하도록 구성.

HttpSession을 이용하여 setAttribute()를 사용자 공간에 'loginInfo'라는 이름으로 문자열을 보관하는 부분이 가장 중요.

로그인된 후에는 로그인한 사용자만 접근 가능한 '/todo/list'를 호출하면 로그인한 사용자로 간주되어서 정상적으로 작성화면 보여짐.

 

 

> login.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<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>

 

 

▷ 필터를 이용한 로그인 체크

- 로그인 여부를 체크해야하는 컨트롤러마다 동일하게 체크하는 로직을 작성하면 같은 코드를 계속 반복 작성하기 때문에 대부분은 필터(Servlet Filter)라는 것을 이용해서 처리.

- 필터는 특정한 서블릿이나 JSP 등에 도달하는 과정에서 필터링하는 역할을 위해서 존재하는 객체.

- @WebFilter 어노테이션을 이용해서 특정한 경로에 접근할 때 필터가 동작하도록 설계하면 동일한 로직을 필터로 분리할 수 있음.

- 필터는 여러개 적용 가능.

 

 

> LoginCheckFilter

- filter라는 패키지를 만들고 LoginCheckFilter 클래스 추가.

// Log4j2, servlet, io 임포트

@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......");
        
        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);
    }
}

Filter 인터페이스에는 doFilter라는 추상 메서드가 존재하는데, 이는 필터가 필터링이 필요한 로직을 구현하는 부분임.

@WebFilter는 특정한 경로를 지정해서 해당 경로의 요청에 대해 doFilter()를 실행하는 구조임. 위 클래스의 경우 '/todo/*'로 지정되어 브라우저에서 '/todo/...'로 시작하는 모든 경로에 대해서 필터링을 시도함.

↳ Filter 인터페이스의 doFilter()는 HttpServletRequest, HttpServletResponse보다 상위 타입의 파라미터를 사용하므로 HTTP와 관련된 작업을 위하여 다운그레이드함.

- 필터 작업 마지막에는 다음 필터나 목적지(Servlet, JSP)로 갈 수 있도록 FilterChain의 doFilter() 실행.

 

 

> UTF-8 처리 필터

- 한글 인코딩이 깨지는 상황은 많이 존재하기 때문에 필터로 처리해두면 매번 같은 기능을 개발하지 않아도 됨. (HttpServletRequest 데이터를 setCharacterEncoding("UTF-8") 적용하는 것)

@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);
    }
}

 

 

> 세션 이용한 로그아웃 처리 (LogoutController)

- HttpSession은 로그아웃 처리는 간단하게 로그인 확인 시에 사용했던 정보를 삭제하는 방식으로 구현하거나 현재의 HttpSession이 더이상 유효하지 않다고 invalidate() 시키는 방식을 이용함.

@WebServlet("/logout")
@Log4j2
public class LogoutController extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws Exception {
        log.info("log out......");
        
        HttpSession session = req.getSession();
        
        session.removeAttribute("loginInfo");
        session.invalidate();
        
        resp.sendRedirect("/");
    }
}

↳ '/logout'은 중요한 처리 작업이기 때문에 POST 방식인 경우에만 동작하도록 doPost()로 설계.

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

 

 

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

- 멤버 테이블 생성

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

 

- 테이블에 사용자 추가

insert into tbl_member (mid, mpw, mname) values ('user0', '1111', 'SuldenLion');
insert into tbl_member (mid, mpw, mname) values ('user1', '2222', 'SuldenTiger');

 

- VO와 DAO 구현

@Getter
@ToString
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class MemberVO {
    private String mid;
    private String mpw;
    private String mname;
}
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;
    }
}

 

- DTO와 Service

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class MemberDTO {
    private String mid;
    private String mpw;
    private String mname;
}
@Log4j2
public enum MemberService {
    INSTANCE;
    
    private MemberDAO dao;
    private ModelMapper modelMapper;
    
    MemberService() {
        dao = new MemberDAO();
        modelMapper = MapperUtil.INSTANCE.get();
    }
    
    public MemberDTO login(String mid, String mpw) throws Exception {
        MemberVO vo = dao.getWithPassword(mid, pwd);
        MemberDTO memberDTO = modelMapper.map(vo, MemberDTO.class);
        
        return memberDTO;
    }
}

↳ MemberService는 여러 곳에서도 동일한 객체를 사용할 수 있도록 enum으로 하나의 객체만을 구성하고 MemberDAO를 이용하도록 구성.

 

 

> Controller에서 Service를 통해 로그인 연동

@WebServlet("/login")
@Log
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);
    }
    
    @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;
        
        try {
            MemberDTO memberDTO = MemberService.INSTANCE.login(mid, mpw);
            HttpSession session = req.getSession();
            session.setAttribute("loginInfo", str);
            resp.sendRedirect("/todo/list");
        } catch (Exception e) {
            resp.sendRedirect("/login?result=error");
        }
        
        resp.sendRedirect("/todo/list");
    }
    
}

변경 내용

↳ 정상적으로 로그인된 경우 HttpSession을 이용해서 'loginInfo' 이름으로 객체 저장

↳ 예외 발생의 경우 '/login'으로 이동. 이동하면서 result라는 파라미터를 전달하여 문제 발생 사실을 같이 전달.

 

> EL에서 쿼리 스트링 처리

- login.jsp에는 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">
    <button type="submit">LOGIN</button>
</form>

</body>
</html>

 

> EL의 Scope와 HttpSession 접근하기

- EL을 이용하면 HttpServletRequest에 setAttribute()로 저장한 객체 사용 가능. 

- EL은 특별하게도 HttpServletRequest에 저장된 객체를 찾을 수 없다면 자동으로 HttpSession에서 저장된 객체를 찾아내는 방식으로 동작. (= EL의 스코프)

- EL의 scope는 HttpServletRequest나 HttpSession 등에서 setAttribute()로 되어있는 데이터를 찾을 때 사용. 

 

 EL 스코프를 이용해서 접근하는 4가지 변수 :

- Page Scope : JSP에서 EL을 이용해 <c:set>으로 저장한 변수

- Request Scope : HttpServletRequest에 setAttribute()로 저장한 변수

- Session Scope : HttpSession을 이용해서 setAttribute()로 저장한 변수

- Application Scope : ServletContext를 이용해서 setAttribute()로 저장한 변수

 

- EL로 ${obj}라고 할 때, 스코프들이 순차적으로 Page → Request → Session → Application의 순서대로 'obj'라는 이름의 객체를 찾는 방식

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

// 출력 결과
// > MemberDTO(mid=user0, mpw=1111, mname=유저0)
// > 유저0

 

 

▶ 사용자 정의 쿠키

- JSESSIONID와 같은 쿠키는 개발자가 직접 정의하지 않는 세션 쿠키라는 이름으로 구분되고, 일반적인 쿠키는 개발자의 필요에 의해서 생성되어 브라우저에 전송하는 '사용자 정의 쿠키'를 뜻함.

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

↳ 개발자가 직접 쿠키를 생성할 때는 newCookie()를 이용하며 이때 반드시 문자열로 된 이름(name)과 값(value)이 필요함. 값은 일반적인 문자열로 저장 불가하므로 URLEncoding된 문자열로 저장해야함.

 

 

▷ 쿠키를 사용하는 경우

- 쿠키는 서버와 브라우저 사이를 오가기 때문에 보안에 취약함 (제한적인 용도로 쿠키 사용)

- 오랜 시간 보관해야 하는 데이터는 항상 서버에 보관하고, 약간의 편의를 제공하기 위한 데이터는 쿠키로 보관하는 방식 사용. (ex. '오늘 하루 이 창 열지 않기', '최근 본 상품 목록' 등 서버에 보관할 필요없는 사소한 데이터는 쿠키를 이용해서 처리)

- 쿠키의 대표적인 사용은 '자동 로그인' 기능. 쿠키의 유효기간을 지정하는 경우 브라우저가 종료되더라도 보관되는 방식으로 동작하게 되는데 사용자가 매번 로그인하는 수고로움을 덜어줄 수 있게함.

 

 

> 조회한 데이터 쿠키를 이용해서 보관하기

 작업 방식 : 

- 브라우저에서 전송된 쿠키가 있는지 확인 후 있다면 해당 쿠키의 값(value)을 활용하고 없다면 새로운 문자열 생성

- 쿠키의 이름은 viewTodos로 지정

- 문자열 내에 현재 Todo의 번호를 문자열로 연결

- '2-3-4-'와 같은 형태로 연결하고 이미 조회한 번호는 추가하지 않음

- 쿠키의 유효기간은 24시간으로 지정하고 쿠키를 담아서 전송

 

 

> TodoReadController

- 현재 요청에 있는 모든 쿠키 중에 조회 목록 쿠키(viewTodos)를 찾아내는 메서드 추가

- 특정한 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;
}

doGet()에서 'viewTodos'라는 이름의 쿠키를 찾고(findCookie()), 쿠키의 내용물을 검사한 후에 조회한 적이 없는 번호라면 쿠키의 내용물을 갱신해서 브라우저로 보내줌. (쿠키 변경시 다시 경로와 유효기간 세팅)

코드 적용시 조회했던 번호들은 '1-3-5-' 같은 쿠키 형태로 보관되고 24시간 동안 유지됨. ('조회수'나 '최근 본 상품 목록' 처리 가능)

 

 

▷ 쿠키와 세션 동시 활용

자동 로그인 

- 로그인한 사용자의 정보를 쿠키에 보관하고 이를 이용해서 사용자의 정보를 HttpSession에 담는 방식.

- 자동 로그인을 위해서는 쿠키에 어떤 값을 보관해야 할 것인지를 결정해야 하고, 이 값의 유효시간도 고려해야 함.

 

 로그인 구현 방식

- 사용자가 로그인할 때 임의의 문자열을 생성하고 이를 데이터베이스에 보관.

- 쿠키에는 생성된 문자열을 값으로 삼고 유효기간은 1주일로 지정.

- (보안을 위해 원래는 주기적으로 쿠키의 값을 갱신하는 부분이 추가되어야 함)

 

⦁ 로그인 체크 방식

- 현재 사용자의 HttpSession에 로그인 정보가 없는 경우에만 쿠키를 확인

- 쿠키의 값과 데이터베이스의 값을 비교하고 같다면 사용자의 정보를 읽어와서 HttpSession에 사용자 정보 추가.

 

alter table tbl_member add column uuid varchar(50);

↳ tbl_member 테이블에 임의의 문자열 보관을 위한 uuid 컬럼 추가. ( UUID(=Universally unique identifier)는 범용 고유 식별자로 고유한 번호를 랜덤으로 생성할 떄 사용 )

 

 

> 자동 로그인 처리

- login.jsp에 자동 로그인 여부를 묻는 체크박스 추가

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

 

- LoginController의 doPost()에서 'auto'라는 이름으로 전송되는 값이 'on'인지 확인

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

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

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

 

- rememberMe 변수가 true라면 java.util의 UUID를 이용해서 임의의 번호 생성.

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

 

- rememberMe가 true라면 tbl_member 테이블 사용자의 정보에 uuid를 수정하도록 DAO에 추가적인 기능을 작성.

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();
}

 

- Service에 메서드 추가

public void updateUuid(String mid, String uuid) throws Exception {
    dao.updateUuid(mid, uuid);
}

 

- Controller 에서 로그인 후 반영

@WebServlet("/login")
@Log4j2
public class LoginController extends HttpServlet {
    ..생략
    
    @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");
        
        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");
        }
    }
}

 

> 쿠키 생성 및 전송

 - 쿠키에 들어가야 하는 문자열이 제대로 처리되었다면 브라우저에 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);
    rememberCookie.setPath("/");
    
    resp.addCookie(rememberCookie);
}

 

> 쿠키 값을 이용한 사용자 조회

- 쿠키 안에 UUID로 생성된 값을 저장했다면 쿠키의 값을 이용해서 해당 사용자의 정보를 로딩해오는 기능도 필요.

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;
}

 

> Service에 uuid로 사용자 찾을 수 있게 함수 추가

public MemberDTO getByUUID(String uuid) throws Exception {
    MemberVO vo = dao.selectUUID(uuid);
    
    MemberDTO memberDTO = modelMapper.map(vo, MemberDTO.class);
    
    return memberDTO;
}

 

> LoginCheckFilter에서의 쿠키 체크

 진행 과정 :

- HttpServletRequest를 이용해서 모든 쿠키 중에서 'remember-me' 이름의 쿠키를 검색

- 해당 쿠키의 value를 이용해서 MemberService를 통해 MemberDTO를 구성

- HttpSession을 이용해서 'loginInfo'라는 이름으로 MemberDTO를 setAttribute()

- 정상적으로 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...");
        
        HttpServletRequest req = (HttpServletRequest)request;
        HttpServletResponse resp = (HttpServletResponse)response;
        
        HttpSession session = req.getSession();
        
        if (session.getAttribute("loginInfo") != null) {
            chain.doFilter(request, response);
            return;
        }
        
        Cookie cookie = findCookie(req.getCookies(), "remember-me");
        
        if (cookie == null) {
            resp.sendRedirect("/login");
            return;
        }
        
        log.info("Cookie exist");
        String uuid = cookie.getValue();
        
        try {
            MemberDTO memberDTO = MemberService.INSTANCE.getByUUID(uuid);
            
            log.info("쿠키의 값으로 조회한 사용자 정보 : " + memberDTO);
            if (memberDTO == null) {
                throw new Exception("Cookie value is not valid");
            }
            session.setAttribute("loginInfo", memberDTO);
            chain.doFilter(request, response);
        } catch (Exception e) {
            e.printStackTrace();
            resp.sendRedirect("/login");
        }
    }
    
    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;
    }    
}

 

 

▶ 리스너 (Listener)

- 리스너를 이용하면 어떤 정보가 발생(event)했을때 미리 약속해둔 동작을 수행할 수 있으므로 기존의 코드를 변경하지 않고도 추가적인 기능 수행 가능.

- 스프링 MVC는 리스너를 통해서 동작.

 

▷ 리스너의 개념과 용도

- 옵저버 패턴 : 특정한 변화를 구독(subscribe)하는 객체들을 보관하고 있다가 변화가 발생(publish)하면 구독 객체들을 실행.

ex) 재난 감지 시스템 - 지진 감지 센서가 데이터를 발생(event)하면, 해당 이벤트 관제 센터에 통보됨. 관제 센터에서는 산하 기관들에 메시지를 알려주게 되는데 마지막 산하 기관들이 이벤트 리스너와 같음.

 

 서블릿 API에서 정의해둔 인터페이스들의 작업

- 해당 웹 애플리케이션이 시작되거나 종료될 때 특정한 작업을 수행

- HttpSession에 특정한 작업에 대한 감시와 처리

- HttpServletRequest에 특정한 작업에 대한 감시와 처리

 

> ServletContextListener

- ServletContextListener는 contextInitialized()contextDestroyed()를 오버라이드하고 이 둘의 파라미터로는 특별한 객체인 ServletContextEvent라는 객체가 전달됨. ServletContextEvent를 이용하면 현재 애플리케이션이 실행되는 공간인 ServletContext를 접근할 수 있음.

- ServletContext는 현재의 웹 애플리케이션 내 모든 자원들을 같이 사용하는 공간으로 이 공간에 어떤 값을 저장시 컨트롤러나 JSP등에서 활용 가능.

 

> ServletContextListener와 스프링 프레임워크

- ServletContextListener와 ServletContext를 이용하면 프로젝트가 실행될 때 필요한 객체들을 준비하는 작업을 처리할 수 있음.

- 커넥션 풀을 초기화하거나 TodoService와 같은 객체들을 미리 생성해서 보관할 수 있음.

- 스프링 프레임워크를 웹 프로젝트에서 미리 로딩하는 작업을 처리할 때 ServletContextListener를 이용.

 

▷ 세션 관련 리스너

- HttpSessionListener나 HttpSessionAttributeListener 등은 HttpSession 관련 작업을 감시하는 리스너. 이를 이용해서 HttpSession이 생성되거나 setAttribute() 등의 작업이 이루어질때 이를 감지할 수 있음.

@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);
        }
    }	
}

- LoginListener는 HttpSessionAttributeListener 인터페이스를 구현했는데, 이 인터페이스는 attributeAdded(), attributeRemoved(), attributeReplaced()를 이용해서 HttpSession에 setAttribute()/removeAttribute() 등의 작업을 감지할 수 있음.

 

 

 

내용 - 자바 웹 개발 워크북 참조

반응형

댓글