1. 쿠키의 생성 / 전송
- 서버에서 자동 생성하는 쿠키(JSESSIONID)와 사용자가 정의하는 쿠키의 다른 점
사용자 정의 쿠키 | WAS에서 발행하는 쿠키(JSESSIONID) | |
---|---|---|
생성 | 개발자가 직접 newCookie()로 생성 경로도 지정 가능 | 자동 |
전송 | 반드시 HttpServletResponse에 addCookie()를 통해야 전송 | |
유효기간 | 쿠키 생성 시 초 단위로 지정 가능 | 지정불가 |
브라우저의 보관방식 | 유효기간이 없는 경우 메모리상에만 보관 유효기간이 있는 경우 파일이나 기타 방식으로 보관 |
메모리상에만 보관 |
쿠키의 크기 | 4kb | 4kb |
- 개발자가 newCookie()를 이용해 쿠키를 생성할 때는 문자열로 된 이름과 값이 필요
이때, 값은 일반적인 문자열로 저장이 불가능 하여 URLEncoding된 문자열로 저장해야함(한글 불가능)
1) 쿠키를 사용하는 경우
- 쿠키는 서버와 브라우저 사이를 오가기 때문에 보안에 취약한 단점(이 때문에 쿠키의 용도는 제한적)
- 오랜 기간 보관해야하는 데이터는 서버에, 약간의 편의 제공을 위한 데이터는 쿠키로 보관
(오늘 하루 이 창 열지 않기 또는 최근 본 상품 목록 등은 쿠키를 이용) - 쿠키가 가장 잘 쓰이는 곳은 자동 로그인(쿠키의 유효기간이 지정되면 브라우저가 종료되어도 보관되는 방식)
실습
1) 조회한 Todo 확인하기
- Todo 목록에서 조회했던 Todo 번호(tno) 쿠키들을 이용해서 보관
- 동작 방식
- 브라우저에서 전송된 쿠키가 있는 지 확인, 있다면 해당 쿠키 값 활용, 없다면 새로운 문자열 생성
- 쿠키의 이름은 'viewTodos'
- 문자열 내에 현재 Todo의 번호를 문자열로 연결
- '2-3-4-'와 같은 형태로 연결, 이미 조회한 번호는 추가 x
- 쿠키의 유효기간으 24시간으로 하고, 쿠키를 담아서 전송
- TodoController에 추가할 코드
- 현재 요청에 있는모든 쿠키 중 조회 목록 쿠키(viewTodos)를 찾아내는 메서드
- 특정 tno가 쿠키의 내용물이 있는지 확인하는 코드
// TodoReadController
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;
import java.io.IOException;
@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 {
...
// 쿠키 찾기
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);
// 조회했던 번호(tno)를 계속 추가
if(!exist) {
todoListStr += tno + "-";
viewTodoCookie.setValue(todoListStr);
viewTodoCookie.setMaxAge(60*60*24);
viewTodoCookie.setPath("/");
resp.addCookie(viewTodoCookie);
}
...
// 쿠키가 쿠키 목록에 존재하는지 찾아보는 함수
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. 쿠키와 세션을 같이 활용하기
- 작성된 코드는 '/todo/...'로 시작하는 모든 경로에 로그인이 필요하기 때문에 매번 로그인해야 하는 불편함 존재
- 자동 로그인은 로그인한 사용자의 정보를 쿠키에 보관하고 이를 이용해서 사용자의 정보를 HttpSession에 담는 방식
1) 자동 로그인 준비
- 쿠키에 어떤 값을 보관하게 할 것인지 결정, 유효시간도 고려
- 로그인 구현 방식
- 사용자가 로그인할 때 임의의 문자열 생성하고 이를 데이터베이스에 보관
- 쿠키에는 생성된 문자열을 값으로 삼고 유효기간은 일주일로 지정
- 로그인 체크 구현 방식
- 현재 사용자의 HttpSession에 로그인 정보가 없는 경우에만 쿠키 확인
- 쿠키의 값과 데이터베이스의 값을 비교하고, 같다면 사용자의 정보를 읽어와서 HttpSession에 사용자 정보 추가
- 구현을 위해 tbl_member 테이블에 임의의 문자열을 저장할 uuid 칼럼 추가
// database console_
alter table tbl_member add column uuid varchar(50);
실습
2) 자동 로그인 처리
- login.jsp에 자동 로그인 여부를 묻는 체크박스를 추가
// 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>
...
- 로그인을 처리하는 LoginController의 doPost()에서 'auto'라는 이름으로 체크박스에서 전송되는 값이 'on'인지 확인
// loginController
package org.zerock.w2.controller;
import lombok.extern.java.Log;
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.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.UUID;
@WebServlet("/login")
@Log
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");
// rememberMe라는 변수가 true(auto 값이 있고 그 값이 "on"이라면, 즉 자동 로그인이 설정되어 있다면)
// UUID(java.util 중 임의의 숫자 생성하는 도구)를 이용해서 임의의 번호 생성
if(rememberMe) {
String uuid = UUID.randomUUID().toString();
}
...
}
- MemberVO, MemberDTO의 수정: uuid가 추가되었으므로 각각에 'private String uuid;' 추가
- rememberMe가 true라면 tbl_member 테이블에 사용자의 정보에 uuid를 수정하도록 MemberDAO에 추가 기능 작성
// MemberDAO
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 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();
}
}
- MemberService에도 메서드 추가
// MemberService
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;
...
public void updateUuid(String mid, String uuid) throws Exception {
dao.updateUuid(mid, uuid);
}
}
- LoginController에서 위 과정을 로그인 후에 반영하도록 설정
// LoginController
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.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.UUID;
@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);
// rememberMe라는 변수가 true(auto 값이 있고 그 값이 "on"이라면, 즉 자동 로그인이 설정되어 있다면)
// UUID(java.util 중 임의의 숫자 생성하는 도구)를 이용해서 임의의 번호 생성
if(rememberMe) {
String uuid = UUID.randomUUID().toString();
MemberService.INSTANCE.updateUuid(mid, uuid);
memberDTO.setUuid(uuid);
}
// 정상적으로 로그인 된 경우, HttpSession을 이용해서 'loginInfo'라는 이름으로 객체 저장
HttpSession session = req.getSession();
session.setAttribute("loginInfo", memberDTO);
resp.sendRedirect("/todo/list");
} catch (Exception e) { // 예외 발생 시 '/login'으로 이동, result라는 파라미터를 전달해서 문제가 발생했다는 사실 전달
resp.sendRedirect("/login?result=error");
}
}
}
- 결과
- 쿠키의 생성 및 전송: 쿠키에 들어가야 하는 문자열이 제대로 처리되었다면 브라우저에 'remember-me' 이름의 쿠키 생성해서 전송
// LoginController에서 rememberMe 변수가 있을 때 수행하는 코드 부분 변경
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);
}
- 결과
- 쿠키의 값을 이용한 사용자 조회
- MemberDAO에 selectUUID() 기능 추가
// MemberDAO
public MemberVO selectUUID(String uuid) throws Exception {
String query = "select mid, mpw, manme, 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;
}
- MemberService에서 uuid 값으로 사용자를 찾을 수 있도록 getByUUID() 추가
// MemberService
public MemberDTO getByUUID(String uuid) throws Exception {
MemberVO vo = dao.selectUUID(uuid);
MemberDTO memberDTO = modelMapper.map(vo, MemberDTO.class);
return memberDTO;
}
- LoginCheckFilter에서 쿠키 체크
- 원래 HttpSession에 'loginInfo'라는 이름으로 객체가 저장되고 이 객체의 여부만 확인하면 되었지만,
HttpSession에는 없고 쿠키에 UUID 값만 있는 경우 고려
- 진행 과정
- HttpServletRequest를 이용해서 몯느 쿠키 중 'remember-me' 이름의 쿠키 검색
- 해당 쿠키의 value를 이용해서 MemberService를 통해 MemberDTO 구성
- HttpSession을 이용해서 'loginInfo'라는 이름으로 MemberDTO를 setAttribute()
- 정상적으로 FilterChain의 doFilter() 수행
// LoginCheckFilter
package org.zerock.w2.filter;
import lombok.extern.log4j.Log4j2;
import org.zerock.w2.dto.MemberDTO;
import org.zerock.w2.service.MemberService;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.Arrays;
import java.util.Optional;
// '/todo..'로 시작하는 모든 경로에 필터링 시도
@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....");
// doFilter에서 HttpServletRequest와 HttpServletResponse보다 상위 타입의 파라미터 사용
// 따라서 HTTP 관련 작업을 위해 (HttpServlerRequest)request처럼 다운캐스팅 필요
HttpServletRequest req = (HttpServletRequest)request;
HttpServletResponse resp = (HttpServletResponse)response;
HttpSession session = req.getSession();
// LoginController에서 로그인했을 때 세션에 loginInfo라는 이름으로 저장하도록 하였음
// 이 loginInfo가 없다면 로그인하지 않은 것으로 판단되므로 LoginCheckFilter에 의해 '/login' 경로로 이동
if (session.getAttribute("loginInfo") != null) {
// 어떤 값이든 '/login'에서 로그인 정보를 전달해서 로그인이 처리되면 그 이후에는 '/todo/...' 경로 이용가능
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);
// 회원정보를 session에 추가
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;
}
}
- 결과
- HttpSession 내에 loginInfo로 저장된 객체도 없고, remember-me 쿠키도 없는 상황 → '/login' 경로로 redirect
- HttpSession에는 없지만 쿠키가 존재하는 경우 → 정상적으로 '/todo/...' 경로로 이동 가능
- 앞선 방식들의 단점
- 쿠키가 가진 UUID 값에 어느 정도 갱신을 위한 추가 장치 필요(UUID 값을 알면 자동으로 로그인 가능)
- 따라서 주기적으로 UUID 값을 바꾸는 방식을 적용해야 좀 더 안전한 자동 로그인 구현 가능
'back-end > Java' 카테고리의 다른 글
[자바 웹 개발 워크북] 4.1 - 의존성 주입과 스프링 (0) | 2023.01.12 |
---|---|
[자바 웹 개발 워크북] 3.3 - 리스너 (0) | 2023.01.11 |
[자바 웹 개발 워크북] 3.1 - 세션과 필터 (0) | 2023.01.09 |
[자바 웹 개발 워크북] 2.3 - 웹 MVC와 JDBC의 결합 (1) | 2023.01.06 |
[자바 웹 개발 워크북] 2.2 - 프로젝트 내 JDBC 구현 (0) | 2023.01.05 |