/*
*
* 자바웹개발워크북의 내용을 정리하기 위한 포스팅입니다.
* 세션, 쿠키, 서블릿컨텍스트, 필터
*/
1. 세션 트랙킹 : 웹은 기본적으로 과거의 상태를 유지하지 않는 무상태연결입니다. 따라서 과거의 요청 기록을 알 수 없습니다.
HTTP가 무상태를 선택한 가장 큰 이유는 적은 자원으로 여러개의 요청을 처리할 수 있기때문이지만 덕분에 과거의 방문기록을 추적하는 기법이 필요하게 됩니다.이러한 기법들을 세션 트랙킹(session tracking)이라고 합니다.
2. 쿠키 : HTTP에서 세션트랙킹은 '쿠키(Cookie)'라는 존재를 이용합니다.
쿠키는 문자열로 만들어진 데이터의 조각으로 서버와 브라우저 사이에서 요청이나 응답시에 주고받는 형태로 사용됩니다.
쿠키는 문자열로 되어있는 정보로 기본적으로 '이름'과 '값'의 구조를 갖고있습니다.
3. 쿠키를 주고받는 기본 시나리오
▶ 브라우저에서 최초로 서버를 호출하는 경우에 해당 서버에서 발행한 쿠키가 없다면 브라우저는 아무것도 전송하지 않습니다.
▶ 서버에서는 응답메시지를 보낼때 브라우저에게 쿠키를 보내주는데 'SetCookie'라는 HTTP헤더를 이용합니다.
▶ 브라우저는 쿠키를 받은후에 정보를 읽고, 이를 파일형태로 보관할지 메모리상에서만 처리할지 결정합니다.
(이 판단은 쿠키에 있는 유효기간(만료기간)을 보고 판단합니다.
▶ 브라우저가 보관하는 쿠키는 다음에 다시 브라우저가 서버에 요청할때 HTTP헤더에 'Cookie'라는 헤더이름과 함께 전달합니다.
(쿠키에는 경로를 지정할 수 있어서 해당 경로에 맞는 쿠키가 전송)
▶ 서버에서는 필요에 따라서 브라우저가 보낸 쿠키를 읽고 이를 사용합니다.
4. 쿠키를 생성하는 방법
▶ 서버에서 자동으로 생성하는 쿠키 :
■ 응답메시지를 작성할때 정해진 쿠키가 없는경우 자동으로 발행
■ WAS에서 발행되며 이름은 WAS마다 고유한 이름을 사용해서 쿠키를 생성합니다.(톰캣은 'JSESSIONID')
■ 서버에서 발행하는 쿠키는 기본적으로 브라우저의 메모리상에 보관합니다.(브라우저 종료시 쿠키삭제)
■ 서버에서 발행하는 쿠키의 경로는 '/'로 지정됩니다.
▶ 개발자가 생성하는 쿠키 :
■ 이름을 원하는대로 지정할 수 있습니다.
■ 유효기간을 지정할 수 있습니다.(유효기간이 지정되면 브라우저가 파일형태로 보관)
■ 반드시 직접 응답에 추가해 주어야 합니다.
■ 경로나 도메인등을 지정할 수 있습니다.(특정한 서버의 경로를 호출하는 경우에만 쿠키를 사용)
5. 서블릿컨텍스트와 세션저장소
▶ 서블릿컨텍스트(ServletContext) :
웹애플리케이션은 서블릿컨텍스트라는 자신만의 고유 메모리공간을 생성해 서블릿이나 JSP등을 인스턴스로 만들어 서비스를 제공합니다.
▶ 세션저장소 (Session Repository):
웹애플리케이션을 생성할때 톰캣이 발행하는 쿠키(세션쿠키)들을 관리하기위한 메모리영역인 세션저장소를 하나더 생성합니다.
세션저장소는 기본적으로 '키'와 '값'을 보관하는 구조입니다.(이때 키가되는 역할은 톰캣의 경우 JSESSIONID)
문제는 새로운 JSESSIONID쿠키가 만들어질때마다 메모리공간을 차지해야한다는 점입니다.
이문제를 해결하기위해 톰캣은 주기적으로 세션저장소를 조사하면서 사용하지않는 값을을 정리하는 방식으로 동작합니다.
(값을 정리하는 방식은 session-timeout설정을 이용, 톰캣의 경우 기본은 30분)
서블릿API에서는 HttpServletRequest를 통해 getSession()메소드로 각 JSESSIONID공간에 접근할수 있습니다.
6. HTTPServletRequest의 getSession()
▶ JSESSIONID가 없는 경우 : 세션저장소에 새로운 번호로 공간을 만들고 해당 공간에 접근할 수 있는 객체를 반환.
새로운 번호는 브라우저에 JSESSIONID의 값으로 전송(세션쿠키)
▶ JSESSIONID가 있는 경우 : 세션저장소에 JSESSIONID값을 이용해서 할당된 공간을 찾고 이공간에 접근할 수 있는 객체를 반환.
getSession()의 결과물은 세션저장소 내의 공간인데 이공간을 의미하는 타입은 HttpSession타입이라고 하고,
해당공간은 '세션컨텍스트(Session Context) 혹은 세션(Session)'이라고 합니다.
HttpSession타입의 객체를 이용하며 현재 사용자만의 공간에 원하는 객체를 저장하거나 수정/삭제할 수 있습니다.
또한 isNew()와 같은 메소드로 새롭게 공간을 만들어 낸것인지 기존의 공간을 재사용하는지를 구분할 수 있습니다.
7. 세션을 이용하는 로그인체크
▶ 사용자가 로그인에 성공하면 HttpSession으로 해당 사용자의 공간(세션컨텍스트)에 특정한 객체를 이름(key)과 함께 저장합니다.
▶ 로그인체크가 필요한 컨트롤러에서는 현재 사용자의 공간에 지정된 이름(key)으로 객체가 저장되어 있는지를 확인합니다.
만일 객체가 존재한다면 해당 사용자는 로그인된 사용자로 간주하고 그렇지 않다면 로그인 페이지로 이동시킵니다.
8. 필터를 이용한 로그인체크
로그인 여부를 체크해야 하는 컨트롤러마다 동일하게 체크하는 로직을 작성하면 같은 코드를 계속 작성해야 하기 때문에
대부분은 필터(Servlet Filter)라는 것을 이용해서 처리합니다.
필터는 말그대로 특정한 서블릿이나 JSP등에 도달하는 과정에서 필터링하는 역할을 위한 서블릿 API의 특별한 객체입니다.
@WebFilter 어노테이션을 이용해서 특정한 경로에 접근할 때 필터가 동작하도록 설계하면 동일한 로직을 필터로 분리할 수 있습니다.
필터적용
@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라는 패키지를 구성하고 LoginCheckFilter클래스를 추가합니다.
▶ Filter인터페이스를 import할때는 javax.servlet의 Filter인터페이스를 사용해야합니다.
▶ Filter인터페이스엔 doFilter()라는 추상메서드가 있는데 필터링이 필요한 로직을 구현하는 부분입니다.
▶ 필터를 적용하기 위해서는 @WebFilter어노테이션은 추가해야합니다.
이것은 특정한 경로를 지정해서 해당 경로의 요청에 대해서 doFilter()를 실행하는 구조입니다.
▶ LoginCheckFilter의 경우 '/todo/*' 는 '/todo/...'로 시작하는 모든 경로에 대해서 필터링을 시도합니다.
▶ doFilter()의 마지막에는 다음필터나 목적지(서블릿,JSP)로 갈 수 있도록 FilterChain의 doFilter()를 실행합니다.
▶ 만일 문제가 생겨서 더이상 진행할 수 없다면 다음단계로 진행하지않고 다른방식으로 리다이렉트 처리를 할 수 있습니다.
▶ doFilter()는 HttpServletRequest/HttpServletResponse보다 상위타입의 파라미터를 사용하므로 다운캐스팅해주어야 합니다.
9. UTF-8 처리필터
현재 POST방식으로 '/todo/register'를 통해서 전달되는 문자열은 한글이 깨진상태로 저장됩니다.
이를 해결하려면 HttpServletRequest의 데이터를 setCharacterEncoding("UTF-8")을 적용해야하는데,
POST방식으로 한글처리 하는곳은 '/todo' 뿐만이 아니기 때문에 필터로 처리해두면 매번 같은기능을 개발하지 않아도 됩니다.
@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);
}
}
10. 세션을 이용하는 로그아웃 처리
HttpSession을 이용하는 경우 로그아웃처리는 간단하게 로그인 확인시에 사용했던 정보를 삭제하는 방식으로 구현하거나
현재의 HttpSession이 더이상 유효하지 않다고 invalidate()시키는 방식을 이용합니다.
@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("/");
}
}
11. 컨트롤러에서 로그인 연동
@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");
}
}
▶ 예외가 발생하는 경우에는 '/login'으로 이동합니다. 이동할때, 'result'라는 파라미터를 전달해서 문제가 발생했다는 사실을 전달힙니다.
▶ login.jsp에는 EL에서 기본으로 제공하는 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>
12. EL의 Scope와 HttpSession접근
EL을 이용해서 HttpServletRequest에 setAttribute()로 저장한 객체를 사용할 수 있음은 확인했습니다.
EL은 특별하게도 HttpServletRequest에 저장된 객체(setAttribute())를 찾을 수 없다면 자동으로 HttpSession에서 저장된 객체를 찾아내는 방식으로 동작합니다. 이것을 EL의 스코프라고 하는데 변수의 범위가 있는것과 같은 개념입니다.
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'라는 이름으로 저장된 객체를 찾는 방식으로 동작합니다.
13. 사용자정의쿠키와 세션쿠키 비교
사용자 정의쿠키 | WAS에서 발생하는 쿠키(세션쿠키) | |
생성 | 개발자가 직접 newCookie()로 생성 경로도 지정가능 |
자동 |
전송 | 반드시 HttpServletResponse에 addCookie()를 통해야만 전송 | |
유효기간 | 쿠키 생성할 때 초 단위로 지정할 수 있음 | 지정불가 |
브라우저의 보관방식 | 유효기간이 없는 경우에는 메모리상에만 보관 유효기간이 있는 경우에는 파일이나 기타방식으로 보관 |
메모리상에만 보관 |
쿠키의 크기 | 4kb | 4kb |
▶ 개발자가 newCookie()로 생성할 때 반드시 문자열로된 이름과 값이 필요합니다. 값은 일반적인 문자열로 저장이 불가능하기 때문에
URLEncoding된 문자열로 저장해야 합니다.(한글저장 불가)
14. 쿠키의 사용예
쿠키는 서버와 브라우저 사이를 오가기 때문에 보안에 취약한 단점이 있습니다. 때문에 쿠키의 용도는 상당히 제한적일 수 밖에 없습니다.
오랜시간 보관해야하는 데이터는 항상 서버에 보관하고, 약간의 편의를 제공하기위한 데이터는 쿠키로 보관합니다.
예를들어 '오늘하루 이 창 열지않기'나 '최근 본 상품 목록'과 같이 사소하여 서버에서 보관할 필요가 없는 데이터들은 쿠키로 처리합니다.
쿠키의 위상이 변하게 된 가장 큰 이유는 모바일에서 시작된 '자동 로그인'입니다.
쿠키의 유효기간을 지정하는 경우 브라우저가 종료되더라도 보관되는 방식으로 동작하게 되는데 모바일에서는 매번 사용자가 로그인하는 수고로움을 덜어줄 수 있게 됩니다.
15. 사용자정의쿠키의 사용
▶ 브라우저에서 전송된 쿠키가 있는지 확인 : 있다면 해당 쿠키의 값을 활용하고 없다면 새로운 문자열 생성
▶ 쿠키의 이름은 'viewTodos'로 지정
▶ 문자열 내에 현재 Todo의 번호를 문자열로 연결
▶ '2-3-4'와 같은 형태로 연결하고 이미 조회한 번호는 추가하지 않음
▶ 쿠키의 유효기간은 24시간으로 지정하고 쿠키를 담아서 전송
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.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@WebServlet(name = "TodoReadController", urlPatterns = "/todo/read")
@Log4j2
public class TodoReadController extends HttpServlet {
TodoService todoService = TodoService.INSTANCE;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException {
log.info("/todo/read GET...");
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) {
log.error(e.getMessage());
throw new ServletException("read GET 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;
}
위처럼 하루동안 조회했던 목록을 이용해서 '조회수'를 처리하거나 '최근 본 상품 목록' 등을 처리할 수 있습니다.
16. 자동 로그인
작성된 코드를 실행하면 '/todo/...'로 시작하는 모든 경로에 대해서 로그인이 필요하기 때문에 매번 로그인해야 하는 불편함이 존재합니다.
이런 경우 쿠키를 이용한 '자동 로그인'을 고민해 볼 필요가 있습니다.
자동 로그인은 로그인한 사용자의 정보를 쿠키에 보관하고 이를 이용해서 사용자의 정보를 HttpSession에 담는 방식입니다.
사실 자동로그인처리를 제대로 작성하려면 많은 것을 고려해야합니다.
자세한 구현은 '스프링부트와 시큐리티'에서 다시 정리하고 예제에서는 간단히 아이디를 검증하는 수준으로 구현해보겠습니다.
▶ 사용자가 로그인할때 임의의 문자열을 생성하고 이를 데이터베이스에 보관
▶ 쿠키에는 생성된 문자열을 값으로 삼고 유효기간은 1주일로 지정
▶ 현재사용자의 HttpSession에 로그인정보가 없는 경우에만 쿠키를 확인
▶ 쿠키의 값과 데이터베이스의 값을 비교하고 같다면 사용자의 정보를 읽어와서 HttpSession에 사용자정보를 추가
(현실적으로 쿠키의 값을 탈취당하면 문제가 되기 때문에 더 안전하려면 주기적으로 쿠키의 값을 갱신하는 부분이 추가되어야합니다만,
우선은 UUID를 이용한 임의의 문자열을 이용하겠습니다. UUID(universally unique identifier)는 범용고유식별자로 고유한 랜덤번호를 생성할때 많이 사용합니다. 자바에서는 java.util패키지를 이용해서 이를 처리할 수 있습니다.)
1) 구현을 위해 tbl_member테이블에 'uuid'컬럼을 추가합니다.
alter table tbl_member add column uuid varchar(50);
2) 로그인화면(login.jsp)에 자동 로그인 여부를 묻는 체크박스를 추가합니다.
<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>
3) 로그인을 처리(LoginController)하는 doPost()에서 'auto'라는 이름으로 체크박스에서 전송되는 값이 'on'인지 확인합니다.
rememberMe라는 변수가 true라면 java.util의 UUID를 이용해서 임의의 번호를 생성합니다.
String auto = req.getParameter("auto");
boolean rememberMe = auto != null && auto.equals("on");
if (rememberMe) {
String uuid = UUID.randomUUID().toString();
}
4) MemberVO, MemberDTO에 새롭게 uuid가 추가되었으므로 반영시켜 줍니다.
5) rememberMe가 true라면 tbl_member테이블에 사용자의 정보에 uuid를 수정하도록 DAO에 추가기능을 작성합니다.
public void updateUuid(String mid, String uuid) throws Exception {
String sql = "update tbl_member set uuid = ? where mid = ?";
Connection connection = ConnectionUtil.INSTANCE.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement(sql);
preparedStatement.setString(1, uuid);
preparedStatement.setString(2, mid);
preparedStatement.executeUpdate();
}
6) MemberService에도 메소드를 추가합니다.
public void updateUuid(String mid, String uuid) throws Exception {
dao.updateUuid(mid, uuid);
}
7) LoginController에서는 로그인 후 이를 반영합니다.
@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);
Cookie remeberCookie = new Cookie("remember-me", uuid);
remeberCookie.setMaxAge(60*60*24*7); // 쿠키의 유효기간은 1주일
remeberCookie.setPath("/");
resp.addCookie(remeberCookie);
}
HttpSession session = req.getSession();
session.setAttribute("loginInfo", memberDTO);
resp.sendRedirect("/todo/list");
} catch (Exception e) {
resp.sendRedirect("/login?result=error");
}
}
}
8) 쿠키안에 UUID로 생성된 값을 저장했다면 쿠키의 값을 이용해서 사용자의 정보를 로딩해 오늘 기능도 필요합니다.
DAO에 selectUUID()기능을 새롭게 추가합니다.
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;
}
9) MemberService에도 uuid값으로 사용자를 찾을 수 있도록 getByUUID()를 추가합니다.
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값만 있는 경우를 고려해야 합니다.
전체 진행과정을 정리해보면 다음과 같은 방식으로 로그인 체크가 이루어집니다.
■ 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;
}
//session에 loginInfo값이 없다면 쿠키를 체크
Cookie cookie = findCookie(req.getCookies(), "remember-me");
//session에도 없고 쿠키도 없다면 그냥 로그인으로
if (cookie == null) {
resp.sendRedirect("/login");
return;
}
// 쿠기가 존재하는 상황이라면
log.info("cookie는 존재하는 상황");
// uuid값
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;
}
}
이 방식의 단점은 쿠키가 가진 UUID값에 어느 정도 갱신을 위한 추가적인 장치가 있어야 한다는 점입니다.
주기적으로 UUID값을 바꾸는 방식을 적용해서 좀 더 안전한 자동 로그인을 구현할 수 있습니다.
17. 리스너
서블릿API에는 리스너라는 이름이 붙은 특별한 인터페이스들이 존재합니다.
리스너객체들은 이벤트라는 특정한 데이터가 발생하면 자동으로 실행되는 특징이 있습니다.
따라서 기존의 코드를 변경하지 않고도 추가적인 기능을 수행할 수 있습니다.
서블릿API는 여러 이벤트에 맞는 리스너들을 인터페이스들로 정의해 두었는데 이를 이용해서 다음과 같은 작업을 처리할 수 있습니다.
▶ 해당 웹 애플리케이션이 시작되거나 종료될때 특정한 작업을 수행
▶ HttpSession에 특정한 작업에 대한 감시와 처리
▶ HttpServletRequest에 특정한 작업에 대한 감시와 처리
1) 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 -----------------");
}
▶ contextInitialized()와 contextDestroyed()에는 파라미터로 ServletContextEvent라는 객체가 전달됩니다.
ServletContextEvent를 이용하면 현재 애플리케이션이 실행되는 공간인 ServletContext를 접근할수 있습니다.
ServletContext는 쉽게말해서 현재의 웹 애플리케이션 내 모든 자원들을 같이 사용하는 공간이므로, 이 공간에 저장하면 모든 컨트롤러나 JSP등에서 이를 활용할수 있게됩니다.
▶ ServletContext에는 setAttribute()를 이용해서 원하는 이름으로 객체를 보관할 수 있습니다.
ex)servletContext.setAttribute("appName", "w2"); // appName이라는 이름으로 w2라는 이름을 지정
이렇게 ServletContext를 이용해서 저장된 객체는 서블릿 JSP/EL에 공유해서 사용할수 있습니다.
특히 EL의 경우 &{appName}과 같이 단순한 이름만을 이용해도 활용할수 있습니다.
▶ HttpServletRequest에는 getServletContext() 메소드를 이용해서 ServletContext를 이용할수 있습니다.
▶ ServletContextListener와 ServletContext를 이용하면 프로젝트가 실행될때 필요한 객체들을 준비하는 작업을 처리할수 있습니다.
ServletContextListener를 설명하는 진짜 이유는 스프링프레임워트와 관련이 있기 때문입니다. 스프링프레임워크를 웹프로젝트에서 미리 로딩하는 작업을 처리할때 ServletContextListener를 이용합니다.
2) 세션 관련 리스너
서블릿의 리스너중에는 HttpSession관련 작업을 감시하는 리스너들을 등록할수 있습니다.
HttpSessionListener나 HttpSessionAttributeListener등이 그러한 예인데, 이를 이용하여 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() 등의 작업을 감지할 수 있습니다.
'개발 > JAVA' 카테고리의 다른 글
[자바웹개발워크북] 스프링 MVC 구현 - 환경설정, CRUD (0) | 2023.01.17 |
---|---|
[자바웹개발워크북] 4-2. 스프링 Web MVC (0) | 2023.01.14 |
[자바웹개발워크북] 4-1.스프링과 MyBaits (0) | 2023.01.02 |
[자바웹개발워크북] 2. 웹과 데이터베이스 (0) | 2022.12.24 |
[자바웹개발워크북] 1. 웹프로그래밍의 시작 (0) | 2022.12.16 |