snorlax1106 2022. 8. 9. 15:40

RememberMe를 구현하기 위해서는 쿠키와 세션을 이용해야 한다.

쿠키와 세션은 모두 사용자의 정보(데이터)를 저장하기 위해 사용된다.

쿠키와 세션은 각 특징에 따라 조금 다르게 이용이 된다.

[ 쿠키와 세션의 특징 ] 

• 쿠키

  1. 브라우저에 저장(사용자 PC)되어 서버에 부담이 없고, 서버 다중화에 유리하지만, 보안에 불리하다.
  2. 세션과 달리 여러 서버로 전송 가능하다.
  3. 사용자의 로컬에 저장되어있다가 브라우저가 요청시 같이 움직인다.(보안에 취약한 이유)
  4. 세션이 브라우저 단위로 생성되어 브라우저 종료시 사라지는데 반해, 쿠키는 유효시간을 설정이 가능하다. (유효시간 미지정시 브라우저를 종료할때 같이 사라짐)

• 세션

  1. 서버에 저장되어 서버에 부담이 있고, 서버 다주화에 불리하지만, 보안에 유리하다.
  2. 세션의 기본단위는 브라우저이다(기본 유효시간은 30분이지만 브라우저 종료시 세션도 종료됨).

[ 쿠키와 세션을 이용한 자동 로그인 ] 

각 클라이언트의 아이디를 기억하기 위해서는 각 브라우저 단위로 생성되어 브라우저 종료시 함께 종료되는 세션보다 유효시간을 설정할 수 있는 쿠키를 이용하는 것이 맞지만 쿠키만을 사용하여 쿠키에 로그인 정보를 담고 있다면, 보안상 굉장히 취약하다. 따라서 rememberMe와 같은 자동로그인 기능을 구현할 때 쿠키에 로그인 정보 대신 세션아이디를 넣어서 세션과 쿠키를 함께 사용하여 구현하는 것이 바람직하다고 볼 수 있다.

 

[ 쿠키와 세션을 이용한 자동 로그인 순서 ]

  1. session Id, 유효시간, user Id를 가지는 데이터베이스를 생성
  2. RememberMe가 체크 상태이면, 로그인 과정 중에 session Id, 유효시간(7일), user Id를 데이터베이스에 넣고, 쿠키에느 세션 아이디를 기억할 수 있도록 하고, 쿠키의 유효시간(7일)을 지정해준다.
  3. 브라우저 종료 후 다시 같은 브라우저로 요청이 왔을때 쿠키에 있는 session Id를 통해 데이터베이스를 확인하여 user Id를 가져와 user Id를 이용하여 로그인 기능 실행

[ RememberMe 구현 ]

1. sessionId, userId, sessionLimit(유효시간)을 저장하고 있는 데이터 베이스 구축

2. SessionDto 작성

< SessionDto.java >

package com.sju.dsBBS.domain;

import com.mysql.cj.protocol.x.Notice;

import java.util.Date;

public class SessionDto {
    private String sessionId;
    private String userId;
    private Date sessionLimit;

    public SessionDto() {}
    public SessionDto(String sessionId, String userId, Date sessionLimit) {
        this.sessionId = sessionId;
        this.userId = userId;
        this.sessionLimit = sessionLimit;
    }

    public String getSessionId() {
        return sessionId;
    }

    public void setSessionId(String sessionId) {
        this.sessionId = sessionId;
    }

    public Date getSessionLimit() {
        return sessionLimit;
    }

    public void setSessionLimit(Date sessionLimit) {
        this.sessionLimit = sessionLimit;
    }

    public String getUserId() {
        return userId;
    }

    public void setUserId(String userId) {
        this.userId = userId;
    }

    @Override
    public String toString() {
        return "SessionDto{" +
                "sessionId='" + sessionId + '\'' +
                ", sessionLimit=" + sessionLimit +
                ", userId='" + userId + '\'' +
                '}';
    }
}

3. sessionMapper 작성

sessionMapper 작성을 위한 alias 생성을 위해 mybatis에 alias 등록

<typeAlias alias="SessionDto" type="com.sju.dsBBS.domain.SessionDto"/>

< sessionMapper.xml >

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.sju.dsBBS.dao.SessionMapper">

    <select id="select" parameterType="String" resultType="SessionDto">
        select sessionId, userId, sessionLimit
        from session
        where sessionId=#{sessionId}
    </select>

    <insert id="insert" parameterType="SessionDto">
        insert into session
            (userId, sessionId, sessionLimit)
        values
            (#{userId}, #{sessionId}, #{sessionLimit})
    </insert>

    <delete id="delete" parameterType="String">
        delete from session
        where sessionId=#{sessionId}
    </delete>

    <update id="update" parameterType="map">
        update session
        set sessionLimit=#{sessionLimit}
        where sessionId=#{sessionId}
    </update>

    <delete id="deleteAll">
        delete from session
    </delete>


    <select id="count" resultType="int">
        select count(*) from session
    </select>

</mapper>

4. SessionDao 작성

< SessionDaoImpl.java >

package com.sju.dsBBS.dao;

import com.sju.dsBBS.domain.SessionDto;
import org.apache.ibatis.session.SqlSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

@Repository
public class SessionDaoImpl implements SessionDao {
    @Autowired
    private SqlSession session;
    private static String namespace = "com.sju.dsBBS.dao.SessionMapper.";

    @Override
    public SessionDto select(String sessionId) throws Exception {
        return session.selectOne(namespace+"select", sessionId);
    }

    @Override
    public int insert(SessionDto dto) throws Exception {
        return session.insert(namespace+"insert", dto);
    }

    @Override
    public int delete(String sessionId) throws Exception {
        return session.delete(namespace+"delete", sessionId);
    }

    @Override
    public int update(String sessionId, Date sessionLimit) throws Exception {
        Map map = new HashMap();
        map.put("sessionId", sessionId);
        map.put("sessionLimit", sessionLimit);
        return session.update(namespace+"update", map);
    }

    @Override
    public int deleteAll() throws Exception{
        return session.delete(namespace+"deleteAll");
    }

    @Override
    public int count() throws Exception {
        return session.selectOne(namespace+"count");
    }
}

+ Test

더보기

< SessionDaoImplTest.java >

package com.sju.dsBBS.dao;

import com.sju.dsBBS.domain.SessionDto;
import org.apache.ibatis.session.SqlSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

@Repository
public class SessionDaoImpl implements SessionDao {
    @Autowired
    private SqlSession session;
    private static String namespace = "com.sju.dsBBS.dao.SessionMapper.";

    @Override
    public SessionDto select(String sessionId) throws Exception {
        return session.selectOne(namespace+"select", sessionId);
    }

    @Override
    public int insert(SessionDto dto) throws Exception {
        return session.insert(namespace+"insert", dto);
    }

    @Override
    public int delete(String sessionId) throws Exception {
        return session.delete(namespace+"delete", sessionId);
    }

    @Override
    public int update(String sessionId, Date sessionLimit) throws Exception {
        Map map = new HashMap();
        map.put("sessionId", sessionId);
        map.put("sessionLimit", sessionLimit);
        return session.update(namespace+"update", map);
    }

    @Override
    public int deleteAll() throws Exception{
        return session.delete(namespace+"deleteAll");
    }

    @Override
    public int count() throws Exception {
        return session.selectOne(namespace+"count");
    }
}

5. SessionService 작성

< SessionServiceImpl.java >

package com.sju.dsBBS.service;

import com.sju.dsBBS.dao.SessionDao;
import com.sju.dsBBS.dao.UserDao;
import com.sju.dsBBS.domain.SessionDto;
import com.sju.dsBBS.domain.UserDto;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Calendar;

@Service
public class SessionServiceImpl implements SessionService {
    @Autowired
    SessionDao sessionDao;
    @Autowired
    UserDao userDao;

    @Override
    @Transactional(rollbackFor = Exception.class)
    public int addUpdateSessionData(SessionDto sessionDto) throws Exception {
        if (sessionDao.select(sessionDto.getSessionId()) == null) {
            return sessionDao.insert(sessionDto);
        } else {
            return sessionDao.update(sessionDto.getSessionId(), sessionDto.getSessionLimit());
        }
    }

    // cookie에 있는 id에 따른
    @Override
    @Transactional
    public String checkSessionAndUpdate(String sessionId) throws Exception{
        SessionDto sessionDto;

        // cookie에 있는 session id와 session database에 있는 session id가 일치하는 것이 있는지 확인
        sessionDto = sessionDao.select(sessionId);

        if(sessionDto==null)
            return null;

        // 일치하는 아이디의 session data가 있으면
        // sessionDto에 7일을 더해서
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(sessionDto.getSessionLimit());
        calendar.add(Calendar.DATE,7);

        // 세션 Data update
        sessionDao.update(sessionDto.getSessionId(), calendar.getTime());

        return sessionDto.getUserId();
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public int remove(String sessionId) throws Exception {
        return sessionDao.delete(sessionId);
    }
}

addUpdateSessionData 

 매개변수 SessionDto가 SessionDatabase에 없는 SessionId라면 insert, 있는 SessionId라면 SessionLimit update

checkSessionAndUpdate

 매개변수 sessionId가 SessionDatabase에 없는 SessionId라면 null값 return, 있는 sessionId라면 현재시간+7로 update

remove

 매개변수 sessionId를 SessionDatabase에서 delete

+Test

더보기

< SessionServiceImplTest.java >

package com.sju.dsBBS.service;

import com.sju.dsBBS.dao.SessionDao;
import com.sju.dsBBS.domain.SessionDto;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import java.util.Calendar;
import java.util.Date;

import static org.junit.Assert.*;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"file:src/main/webapp/WEB-INF/spring/root-context.xml"})
public class SessionServiceImplTest {
    @Autowired
    private SessionService sessionService;
    @Autowired
    private SessionDao sessionDao;

    @Test
    public void addUpdateSessionDataTest() throws Exception {
        SessionDto sessionDto = new SessionDto();

        Calendar calendar = Calendar.getInstance();
        calendar.add(Calendar.DATE, 1);

        sessionDto.setSessionId("testSessionId0");
        sessionDto.setUserId("18011111");
        sessionDto.setSessionLimit(calendar.getTime());

        sessionDao.deleteAll();

        assertTrue(sessionDao.insert(sessionDto)==1);
        assertTrue(sessionDao.count()==1);

        assertTrue(sessionService.addUpdateSessionData(sessionDto)==1);
        System.out.println("(sessionDao.select(\"18011111\").getSessionLimit()) = " + (sessionDao.select("testSessionId0").getSessionLimit()));
        assertTrue(sessionDao.count()==1);

        calendar.add(Calendar.DATE,2);
        sessionDto.setSessionLimit(calendar.getTime());

        assertTrue(sessionService.addUpdateSessionData(sessionDto)==1);
        assertTrue(sessionDao.count()==1);
        System.out.println("(sessionDao.select(\"18011111\").getSessionLimit()) = " + (sessionDao.select("testSessionId0").getSessionLimit()));
        assertTrue(sessionDao.deleteAll()==1);
    }

    @Test
    public void checkSessionAndUpdateTest() throws Exception {
        sessionDao.deleteAll();
        SessionDto sessionDto = new SessionDto("testSessionId0", "18011000", new Date());
        assertTrue(sessionDao.insert(sessionDto)==1);
        assertTrue(sessionDao.count()==1);

        sessionService.checkSessionAndUpdate("testSessionId0");

        assertTrue(sessionDao.deleteAll()==1);
    }

    @Test
    public void removeTest() throws Exception {
        sessionDao.deleteAll();
        SessionDto sessionDto = new SessionDto("testSessionId0", "18011000", new Date());
        assertTrue(sessionDao.insert(sessionDto)==1);
        assertTrue(sessionDao.count()==1);
        sessionService.remove("testSessionId0");
        assertTrue(sessionDao.count()==0);
    }
}

6. RememberMe 체크 상태에 로그인 기능

if (rememberMe) {
    SessionDto sessionDto = new SessionDto();

    Cookie cookie = new Cookie("autoLoginBySessionId", session.getId());
    // 쿠키를 찾을 경로를 컨텍스트 경로로 변경

    cookie.setPath("/");
    cookie.setMaxAge(60*60*24*7);
    response.addCookie(cookie);

    Calendar calendar = Calendar.getInstance();
    calendar.add(Calendar.DATE,7);

    sessionDto.setSessionId(session.getId());
    sessionDto.setSessionLimit(calendar.getTime());
    sessionDto.setUserId(id);

    sessionService.addUpdateSessionData(sessionDto);

}

rememberMe가 true인 경우 쿠키에 autoLoginBySessionId이름의 HttpSession 객체인 session의 아이디(session Id)를 가져와 값으로 넣어준 뒤, 쿠키의 유효기간을 7일로 설정(cookie.setMaxAge(60*60*24*7)) 후 응답에 담아준다.

세션 아이디, 유저 아이디, 세션 유효기간을 sessionDto에 담아 데이터베이스에 추가해준다.

7. HandlerIntercepter Interface를 구현한 RememberInterceptor 작성

< RememberInterceptor.java >

package com.sju.dsBBS.interceptor;

import com.sju.dsBBS.domain.UserDto;
import com.sju.dsBBS.service.SessionService;
import com.sju.dsBBS.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import org.springframework.web.util.WebUtils;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

public class RemeberInterceptor extends HandlerInterceptorAdapter {
    @Autowired
    private SessionService sessionService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        Cookie sessionId = WebUtils.getCookie(request, "autoLoginBySessionId");
        // sessionId가 Cookie에 있으면
        if (sessionId != null) {
            sessionId.setPath("/");
            sessionId.setMaxAge(60*60*24*7);
            response.addCookie(sessionId);
            
            String userId = sessionService.checkSessionAndUpdate(sessionId.getValue());
//            System.out.println("userId = " + userId);
            if(userId != null) {
                HttpSession session = request.getSession();
                session.setAttribute("id", userId);
            }
        }

        return true;
    }
}

요청이 있을 때 cookie에서 "autoLoginBySessionId"이름의 쿠키를 확인해  sessionId가 저장되어있으면 쿠키의 유효기간을 7일로 설정 후 HttpResponse객체에 담아 준 뒤 sessionId와 Session 데이터베이스를 통해 userId를 알아와서 session에 id를 담아준다(로그인 기능과 동일). 

 

servlet-context.xml에 rememberInterceptor를 interceptor로 등록

<interceptors>
   <interceptor>
      <mapping path="/**"/>
         <beans:bean id="remeberInterceptor" class="com.sju.dsBBS.interceptor.RemeberInterceptor"/>
   </interceptor>
</interceptors>

8. logout 구현

@GetMapping("/logout")
public String logout(HttpSession session, HttpServletRequest request, HttpServletResponse response) {
    String userId = (String)session.getAttribute("id");
    if(userId != null) {
        // session 종료
        session.removeAttribute("id");
        session.invalidate();

        Cookie cookie = WebUtils.getCookie(request, "autoLoginBySessionId");
        if(cookie != null) {
            cookie.setPath("/");
            cookie.setMaxAge(0);
            response.addCookie(cookie);

            // session data 삭제(sessionService이용)
            String sessionId = cookie.getValue();
            try {
                sessionService.remove(sessionId);
            } catch (Exception e) {
                e.printStackTrace();
                throw new RuntimeException(e);
            }
        }
    }

    // 2. 홈으로 이동
    return "redirect:/";
}

 

 

session과 autoLoginBysessionId 쿠키 의 sessionId를 통해 session data에 session 정보를 지우고, autoLoginBysession쿠키도 삭제한다.