RememberMe 구현
RememberMe를 구현하기 위해서는 쿠키와 세션을 이용해야 한다.
쿠키와 세션은 모두 사용자의 정보(데이터)를 저장하기 위해 사용된다.
쿠키와 세션은 각 특징에 따라 조금 다르게 이용이 된다.
[ 쿠키와 세션의 특징 ]
• 쿠키
- 브라우저에 저장(사용자 PC)되어 서버에 부담이 없고, 서버 다중화에 유리하지만, 보안에 불리하다.
- 세션과 달리 여러 서버로 전송 가능하다.
- 사용자의 로컬에 저장되어있다가 브라우저가 요청시 같이 움직인다.(보안에 취약한 이유)
- 세션이 브라우저 단위로 생성되어 브라우저 종료시 사라지는데 반해, 쿠키는 유효시간을 설정이 가능하다. (유효시간 미지정시 브라우저를 종료할때 같이 사라짐)
• 세션
- 서버에 저장되어 서버에 부담이 있고, 서버 다주화에 불리하지만, 보안에 유리하다.
- 세션의 기본단위는 브라우저이다(기본 유효시간은 30분이지만 브라우저 종료시 세션도 종료됨).
[ 쿠키와 세션을 이용한 자동 로그인 ]
각 클라이언트의 아이디를 기억하기 위해서는 각 브라우저 단위로 생성되어 브라우저 종료시 함께 종료되는 세션보다 유효시간을 설정할 수 있는 쿠키를 이용하는 것이 맞지만 쿠키만을 사용하여 쿠키에 로그인 정보를 담고 있다면, 보안상 굉장히 취약하다. 따라서 rememberMe와 같은 자동로그인 기능을 구현할 때 쿠키에 로그인 정보 대신 세션아이디를 넣어서 세션과 쿠키를 함께 사용하여 구현하는 것이 바람직하다고 볼 수 있다.
[ 쿠키와 세션을 이용한 자동 로그인 순서 ]
- session Id, 유효시간, user Id를 가지는 데이터베이스를 생성
- RememberMe가 체크 상태이면, 로그인 과정 중에 session Id, 유효시간(7일), user Id를 데이터베이스에 넣고, 쿠키에느 세션 아이디를 기억할 수 있도록 하고, 쿠키의 유효시간(7일)을 지정해준다.
- 브라우저 종료 후 다시 같은 브라우저로 요청이 왔을때 쿠키에 있는 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쿠키도 삭제한다.