▶ 세션과 필터
- 웹은 기본적으로 과거의 상태를 유지하지 않는 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() 등의 작업을 감지할 수 있음.
내용 - 자바 웹 개발 워크북 참조
'Spring' 카테고리의 다른 글
스프링 & 스프링 Web MVC 주요 개념 (0) | 2024.06.23 |
---|---|
Spring Tiles (스프링 타일즈) (0) | 2023.07.25 |
웹 페이지 파일(이미지) 업로드 & 페이지네이션 (0) | 2023.07.24 |
(Spring 관련 정보) Dispatcher Servlet, Filter, Interceptor에 대하여 (0) | 2023.07.18 |
스프링 CRUD Library 프로그램 버전별 정리 後 (0) | 2023.07.17 |
댓글