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

 

  - 결과

로그인 화면에 자동로그인을 설정할 체크박스 생성됨
체크 상태로 로그인하면 uuid가 새로 추가된 모습
데이터베이스의 테이블에도 추가된 uuid 값

 

  - 쿠키의 생성 및 전송: 쿠키에 들어가야 하는 문자열이 제대로 처리되었다면 브라우저에 '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);
}

 

  - 결과

응답 헤더에 Set-Cookie에 remember-me가 전송됨
쿠키도 다음과 같이 확인 가능

 

  - 쿠키의 값을 이용한 사용자 조회

  - 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/...' 경로로 이동 가능

한번 로그인해서 remember-me 쿠키가 생김


 

  • 앞선 방식들의 단점
    • 쿠키가 가진 UUID 값에 어느 정도 갱신을 위한 추가 장치 필요(UUID 값을 알면 자동으로 로그인 가능)
    • 따라서 주기적으로 UUID 값을 바꾸는 방식을 적용해야 좀 더 안전한 자동 로그인 구현 가능

+ Recent posts