로그인 처리하기 - 세션을 직접 만들어서 사용
앞에서 쿠키를 직접 만들어서 로그인 기능을 구현해보았습니다.
세션을 만드는 방법을 알아보기 전에 쿠키의 사용법을 되짚어 보도록 하겠습니다.
쿠키 생성 (서버 -> 클라이언트)
서버에서 쿠키를 생성할 때 member id를 저장해주었습니다.
쿠키의 value type은 String 이라는 점을 알아둡시다.
서버에서 만든 쿠키는 response에 태워서 클라이언트로 전송해주었습니다.
Cookie idCookie = new Cookie("memberId", String.valueOf(loginMember.getId()));
// http response에 쿠키를 넘어서 전달해주자
response.addCookie(idCookie);
클라이언트로 부터 쿠키를 받을 때 (클라이언트 -> 서버)
@CookieValue라는 annotation을 이용하여 받을 수 있었습니다.
Cookie에 값을 넣을 때는 String type이었지만 아래와 같이 annotation을 사용할 때는 자동 형 변환이 가능합니다.
@CookieValue(value = "memberId", required = false) Long memberId
쿠키 삭제 (서버 -> 쿠키 , 로그아웃)
쿠키 삭제하는 방법은 age를 0으로 변경 후 쿠키를 다시 response에 담아서 보내면 됩니다.
Cookie cookie = new Cookie(cookieName, null);
cookie.setMaxAge(0);
response.addCookie(cookie);
세션
1. 세션을 직접 구현해봅시다.
이제는 정말로 세션에 대해서 알아보도록 하겠습니다.
보안상에 문제로 로그인과 같이 중요한 문제는 쿠키보다는 세션을 사용한다고 하였습니다.
세션을 제대로 알아보기 위해 직접 구현해 보도록 하겠습니다.
세션은 기본적으로 서버에 세션id와 그에 해당하는 value를 저장해 놓습니다.
여기에서 value는 사용자(Member 객체)가 될 것이다.
저장하기 위한 세션저장소를 ConcurrentHashMap을 이용하여 만들어보겠습니다.
동시성 문제가 발생할 가능성이 있을 때는 ConCurrentHashMap을 사용해야 한다고 합니다.
private Map<String, Object> sessionStore = new ConcurrentHashMap<>();
진행 과정은 아래와 같습니다.
* session Id 생성 (임의의 추정 불가능한 랜덤 값)
* 세션 저장소에 sessionId와 보관할 값 저장
* sessionId로 응답 쿠키를 생성해서 클라이언트에 전달
세션 ID 생성 및 저장 (서버)
session Id 생성 (임의의 추정 불가능한 랜덤 값) , 값을 세션보관소에 저장을 합니다.
session id는 중복이 없는 universal unique id 를 이용하도록 하겠습니다. (UUID)
만들어 놓은 ConcurrentHashMap에 저장하였습니다.
String sessionId = UUID.randomUUID().toString();
sessionStore.put(sessionId, value);
사용자 정보를 세션 보관소에 value 값으로 저장하였으며
key값은 unique한 숫자의 조합으로 구성되어 있습니다.
그러면 이 key값을 쿠키에 넣어서 사용자에게 전달하도록 하겠습니다.
쿠키 생성 및 전달 (서버 -> 클라이언트)
세션도 결국 쿠키를 생성해서 이용합니다.
세션이 쿠키와 다른 점은 뭘까요?
세션은 중요한 정보를 서버에 저장하는 것이고
쿠키에는 암호화된 키를 저장해서 클라이언트에게 전달한다는 것입니다.
사용자 정보를 그대로 쿠키에 담는 시스템과는 다르죠??
아래와 같이 쿠키를 생성해서 클라이언트에게 전달하였습니다.
Cookie mySessionCookie = new Cookie(SESSION_COOKIE_NAME, sessionId);
response.addCookie(mySessionCookie);
세션 쿠키 조회 (클라이언트 -> 서버)
앞에서 세션 쿠키를 만들어서 클라이언트에게 전달하는 과정까지 완료하였습니다.
이제는 사용자가 우리 서버에 접근할 때, 세션쿠키를 가지고 와서 확인하는 작업을 진행해 볼 것입니다.
클라이언트에게서 쿠키를 전달받고
전달받은 세션id가 서버에 존재하는지 확인해 보겠습니다.
고객이 준 세션쿠키의 sessionId 값이 서버에도 존재한다면 sessionId 에 대응되는 value(Member객체)를 세션보관소에서 꺼내 반환해줍니다.
public Object getSession(HttpServletRequest request) {
Cookie cookie = findCookie(request, SESSION_COOKIE_NAME);
if (cookie == null) {
return null;
}
return sessionStore.get(cookie.getValue());
}
public Cookie findCookie(HttpServletRequest request, String cookieName) {
if (request.getCookies() == null) {
return null;
}
return Arrays.stream(request.getCookies())
.filter(cookie -> cookie.getName().equals(cookieName))
.findAny()
.orElse(null);
}
세션 만료
세션이 만료될 경우 ConcurrentHashMap에서 값을 제거해주자.
public void expire(HttpServletRequest request) {
Cookie cookie = findCookie(request, SESSION_COOKIE_NAME);
if (cookie != null) {
sessionStore.remove(cookie.getValue());
}
}
완성된 코드는 아래와 같습니다.
SessionManager.java
package hello.login.web.session;
import java.util.Arrays;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
/**
* 세션 관리
*/
@Component
public class SessionManager {
public static final String SESSION_COOKIE_NAME = "mySessionId";
// 세션id와 멤버 객체를 저장하는 저장소
private Map<String, Object> sessionStore = new ConcurrentHashMap<>();
/**
* 세션 생성
* session Id 생성 (임의의 추정 불가능한 랜덤 값)
* 세션 저장소에 sessionId와 보관할 값 저장
* sessionId로 응답 쿠키를 생성해서 클라이언트에 전달
*/
public void createSession(Object value, HttpServletResponse response) {
// session Id 생성 (임의의 추정 불가능한 랜덤 값) , 값을 세션에 저장
// universal unique id 를 생성
String sessionId = UUID.randomUUID().toString();
sessionStore.put(sessionId, value);
// 쿠키 생성
Cookie mySessionCookie = new Cookie(SESSION_COOKIE_NAME, sessionId);
response.addCookie(mySessionCookie);
}
/**
* 세션 조회
*/
public Object getSession(HttpServletRequest request) {
Cookie cookie = findCookie(request, SESSION_COOKIE_NAME);
if (cookie == null) {
return null;
}
return sessionStore.get(cookie.getValue());
}
public Cookie findCookie(HttpServletRequest request, String cookieName) {
if (request.getCookies() == null) {
return null;
}
return Arrays.stream(request.getCookies())
.filter(cookie -> cookie.getName().equals(cookieName))
.findAny()
.orElse(null);
}
/**
* 세션 만료
*/
public void expire(HttpServletRequest request) {
Cookie cookie = findCookie(request, SESSION_COOKIE_NAME);
if (cookie != null) {
sessionStore.remove(cookie.getValue());
}
}
}
테스트 코드 작성
테스트 코드를 작성하는 것 또한 중요합니다.
이번에는 테스트 코드를 작성하도록 하겠습니다.
과정은 아래와 같습니다.
임의의 Member 객체 생성
서버 -> 클라이언트 : 세션 발급
클라이언트 -> 서버 : 세션 조회
조회한 세션으로 세션 스토어에서 value값을 찾아옴
value값과 Member 객체 비교
Test를 할 때는 HttpServletRequest와 Response가 필요한데
Mock을 사용하면 한결 테스트가 편해집니다.
class SessionManagerTest {
SessionManager sessionManager = new SessionManager();
@Test
void sessionTest() {
//세션 생성
MockHttpServletResponse response = new MockHttpServletResponse();
Member member = new Member();
sessionManager.createSession(member, response);
//요청에 응답 쿠키 저장
MockHttpServletRequest request = new MockHttpServletRequest();
request.setCookies(response.getCookies());
//세션 조회
Object result = sessionManager.getSession(request);
assertThat(result).isEqualTo(member);
//세션 만료
sessionManager.expire(request);
Object expired = sessionManager.getSession(request);
assertThat(expired).isNull();
}
}
라이브러리를 사용하지 않고 세션을 직접 만들어보았습니다.
우리가 만든 세션을 적용해보도록 하겠습니다.
세션 적용 1
HomeController.java
홈 화면입니다.
session이 존재하면 loginHome 화면을 보여주고
session이 존재하지 않는다면 home 화면을 보여준다.
@GetMapping("/")
public String loginHomeV2(HttpServletRequest request,
Model model) {
Member member = (Member) sessionManager.getSession(request);
if (member == null) {
return "home";
}
model.addAttribute("member", member);
return "loginHome";
}
LoginController.java
로그인 화면입니다.
id가 null이거나 password가 null인 경우 에러메세지가 있는 loginForm을 출력 (로그인 화면을 출력)
if (bindingResult.hasErrors()) {
return "login/loginForm";
}
id와 password가 db상에 존재하지 않는 경우 에러메세지와 함께 loginForm 출력
Member loginMember = loginService.login(form.getLoginId(), form.getPassword());
if (loginMember == null) {
bindingResult.reject("loginFail", "아이디 또는 비밀번호가 맞지 않습니다.");
return "login/loginForm";
}
정확한 아이디와 패스워드를 입력했을 경우 세션 관리자를 통해 세션을 생성하고 response에 세션id를 쿠키에 넣어서 전달하는 과정입니다.
sessionManager.createSession(loginMember, response);
전체 코드입니다.
@PostMapping("/login")
public String loginV2(@Valid @ModelAttribute LoginForm form, BindingResult bindingResult, HttpServletResponse response) {
if (bindingResult.hasErrors()) {
return "login/loginForm";
}
Member loginMember = loginService.login(form.getLoginId(), form.getPassword());
if (loginMember == null) {
bindingResult.reject("loginFail", "아이디 또는 비밀번호가 맞지 않습니다.");
return "login/loginForm";
}
// 로그인 성공 처리 TODO
// 세션 관리자를 통해 세션을 생성하고, 회원 데이터 보관
sessionManager.createSession(loginMember, response);
return "redirect:/";
}
Servlet이 제공하는 HttpSession
Session 개념 이해를 돕기 위해 이때까지 Session을 직접 구현해보았다면, 이번에는 Servlet이 제공하는 HttpSession을 이용해 보도록 하겠습니다.
package hello.login.web;
public class SessionConst {
public static final String LOGIN_MEMBER = "loginMember";
}
로그인을 할 때 마다 새로운 session을 발행해 주는 과정입니다.
아래와 같이 session을 발행하고 session에 정보를 넣는 step으로 진행이 된다.
HttpSession session = request.getSession();
//세션에 로그인 회원 정보 보관
session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);
login 기능 구현은 아래와 같이 만들 수 있을 것입니다.
LoginController.java (세션 발행)
@PostMapping("/login")
public String loginV3(@Valid @ModelAttribute LoginForm form,
BindingResult bindingResult,
HttpServletRequest request) {
if (bindingResult.hasErrors()) {
return "login/loginForm";
}
Member loginMember = loginService.login(form.getLoginId(), form.getPassword());
if (loginMember == null) {
bindingResult.reject("loginFail", "아이디 또는 비밀번호가 맞지 않습니다.");
return "login/loginForm";
}
//로그인 성공 처리
//세션이 있으면 있는 세션 반환, 없으면 신규 세션을 생성
// 기본
// HttpSession session = request.getSession(true);
// session이 없으면 NULL을 반환
// HttpSession session = request.getSession(true);
HttpSession session = request.getSession();
//세션에 로그인 회원 정보 보관
session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);
return "redirect:/";
}
세션 만료
logout은 더욱더 간단합니다.
session.ivalidate() 만 사용하면 됩니다.
@PostMapping("/logout")
public String logoutV3(HttpServletRequest request) {
// 세션이 없으면 NULL을 반환
HttpSession session = request.getSession(false);
if (session != null) {
// 세션과 그 안에 있는 데이터가 다 사라짐
session.invalidate();
}
return "redirect:/";
}
HomeController.java (세션 조회)
세션을 들고 옵니다. (세션이 없다면 생성하지 않습니다.)
세션이 없을 경우 -> Home
로그인에 관련된 세션이 없을 경우 -> Home
로그인에 관련된 세션이 있을경우 -> loginHome
@GetMapping("/")
public String homeLoginV3(HttpServletRequest request, Model model) {
HttpSession session = request.getSession(false);
if (session == null) {
return "home";
}
Member loginMember = (Member)session.getAttribute(SessionConst.LOGIN_MEMBER);
//세션에 회원 데이터가 없으면 home
if (loginMember == null) {
return "home";
}
//세션이 유지되면 로그인으로 이동
model.addAttribute("member", loginMember);
return "loginHome";
}
위 코드는 SessionAttribute를 이용하여 좀 더 간편하게 사용할 수 있습니다.
@GetMapping("/")
public String homeLoginV3Spring(
@SessionAttribute(name = SessionConst.LOGIN_MEMBER, required = false) Member loginMember, Model model) {
//세션에 회원 데이터가 없으면 home
if (loginMember == null) {
return "home";
}
//세션이 유지되면 로그인으로 이동
model.addAttribute("member", loginMember);
return "loginHome";
}
SessionAttribute 어노테이션을 사용하게 되면, 처음 로그인할 때 url에 세션정보가 나타나는 것을 확인할 수 있습니다.
application.properties를 이용하여 해당 url이 나타나지 않도록 적용해 봅시다.
server.servlet.session.tracking-modes=cookie
접속 유지
해당 웹사이트에서 활동을 한지 30분 이후로 세션을 끊는 옵션입니다.
최소시간은 60초입니다.
server.servlet.session.timeout=1800
아래와 같이 직접 설정해 줄 수도 있습니다.
session.setMaxInactiveInterval(1800);
실무에서는 세션에 최소한의 데이터만 보관해야합니다.
보관한 데이터 용량 * 사용자 수로 세션의 메모리 사용량이 급격하게 늘어나서 장애로 이어질 수도 있습니다.
현재는 세션에 Member 객체를 넣고 있지만 Member id, name 정도로 필요한 정도만 넣어서 사용하시길 바랍니다.