회원가입을 하면 memberRepository에 회원 정보가 추가되고 추가된 상태에서 로그인 한다면 로그인 정보(Id, Password)를 통해 memberRepository에 해당 정보가 존재하는 지 확인 후 존재한다면 로그인이 성공한 home을 보여주는 과정을 추가한다.
@GetMapping("/")
public String homeLogin(@CookieValue(name = "memberId", required = false) Long memberId, Model model) {
if (memberId == null) {
return "home";
}
// 로그인
Member loginMember = memberRepository.findById(memberId);
if (loginMember == null) {
return "home";
}
model.addAttribute("member", loginMember);
return "loginHome";
}
homeLogin 컨트롤러는 두 가지 템플릿(home, loginHome)중 하나를 띄워주며 쿠키에 memberId값이 없거나, memberId값이 있지만 이를 통해 memberRespository에서 Member객체가 없다면 로그인이 되지 않은 home인 home 템플릿으로 return하며 memberId값이 있고, 이 값으로 Member객체를 조회할 수 있다면 loginHome 템플릿을 return한다.
//loginController
@GetMapping("/login")
public String loginForm(@ModelAttribute("loginForm") LoginForm form) {
return "login/loginForm";
}
@PostMapping("/login")
public String login(@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";
}
// 로그인 성공 처리
// 쿠키에 시간 정보를 주지 않으면 세션 쿠키(브라우저 종료시 모두 종료!)
Cookie idCookie = new Cookie("memberId", String.valueOf(loginMember.getId()));
response.addCookie(idCookie);
return "redirect:/";
}
GET - /login : /login uri에 get 메서드로 요청 시 로그인 입력 폼 띄움.POST - /login: loginForm에 로그인 정보를 입력하고 제출 버튼으로 하여금 POST 요청을 보내면 로그인 로직이 작동. bindingResult에 에러가 담길 시 loginForm을 다시 랜더링하고(에러와 함께), 로그인 정보 매칭이 성공할 경우 memberId(로그인 저장소의 로그인 정보의 id값)을 쿠키 값을 포함하여 home 컨트롤러가 작동되는 기본 경로로 리다이렉트 시킴.(home 컨트롤러의 인자에 쿠키값이 들어오므로 loginHome 템플릿이 매칭될 것임)Cookie 사용 이유
HTTP 통신은 기본적으로 무상태 프로토콜이다. 하지만 상태 유지가 필요한 경우에 한해 쿠키와 같은 기술을 이용할 수 있다. 쿼리 파라미터를 계속 유지하며 보낼 수도 있지만 이는 매우 어렵고 반복적이며 번거로운 작업이다. 쿠키는 만료날짜를 기준으로 계속해서 해당 브라우저의 세션에 남아있고 요청시 자동으로 담겨 서버에서 이를 지속적으로 인식할 수 있기에 상태관리시에 유용하다.
@PostMapping("/logout")
public String logout(HttpServletResponse response) {
expireCookie(response, "memberId");
return "redirect:/";
}
private void expireCookie(HttpServletResponse response, String cookieName) {
Cookie cookie = new Cookie(cookieName, null);
cookie.setMaxAge(0);
response.addCookie(cookie);
}
현재 로그인의 여부는 쿠키에 만료가 되지 않은 memberId값이 있느냐를 서버가 확인하면 로그인 상태에서 접근 가능한 loginHome을 뿌려주는 상황이다. 그러므로 로그아웃시에는 memberId 쿠키를 만료시키면 된다.
로그인 상태관리를 위한 쿠키 값에 우리는 Member저장소의 Member 고유의 id를 부여했다. 이는 실질적으로 비밀번호를 전 세계에 공개하는 것과 같다. 우리의 서버의 저장소에서 이 id값은 변화하지 않을 것이고, 누군가가 해당 사용자 컴퓨터에 들어와 해당 사용자의 쿠키값이 2라는 것을 알게 되었다면 해커는 2라는 숫자로 계속해서 타인인 척 로그인이 가능하다.
쿠키에는 중요한 값을 노출하지 않아야 하며, 사용자 별로 예측 불가능한 임의의 토큰을 노출하고 서버에서 이를 검증해서 인식하도록 한다. 또한 서버에서 토큰을 관리한다. 임의의 토큰이 해커에 의해 털리더라도 시간이 조금 지나면 사용할 수 없도록 서버에서 해당 토큰의 만료시간을 짧게 유지한다.
세션(Session)은 사용자가 웹 서버에 접속하여 브라우저를 통해 여러 페이지를 요청하고 응답받는 동안 유지되는 상태를 말한다. 보통 사용자가 웹 사이트에 접속하면 서버는 사용자를 식별하기 위해 세션을 생성한다. 세션은 서버 측에서 관리되며, 각 세션에는 고유한 세션 ID가 부여된다.

웹 브라우저가 로그인정보(id, pw)를 넘겼을 때 해당 웹 브라우저의 세션 Id를 생성한다. 그리고 Cookie에 sessionId="UUID 추출값"을 response한다. 그리고 동시에 서버에서는 (sessionId - Member객체)를 보관한다.
웹 브라우저의 쿠키 정책에 따라 브라우저가 완전히 종료되거나 만료될 경우 브라우저의 쿠키 보관소에 sessionId가 없어질 것이다. 이 경우 로그인을 다시 시도하도록 사용자 환경을 구성해야 한다.
세션을 직접 구현하여 쿠키 값으로 memberId를 사용했을 때 변조하여 이를 악용할 경우를 방지했고(예상 불가능한 UUID 사용) 로그인 후 전달해주는 쿠키에는 중요한 정보가 담기지 않으며, 로그인을 위한 sessionId가 탈취되어도 시간이 지나면 사용할 수 없도록 서버에서 만료시간을 짧게 유지했다.
@Component
public class SessionManager {
public static final String SESSION_COOKIE_NAME = "mySessionId";
private Map<String, Object> sessionStore = new ConcurrentHashMap<>();
public void createSession(Object value, HttpServletResponse response) {
// session 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 sessionCookie = findCookie(request, SESSION_COOKIE_NAME);
if (sessionCookie == null) {
return null;
}
return sessionStore.get(sessionCookie.getValue());
}
// 세션 만료 로직
public void expire(HttpServletRequest request) {
Cookie sessionCookie = findCookie(request, SESSION_COOKIE_NAME);
if (sessionCookie != null) {
sessionStore.remove(sessionCookie.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);
}
}
@PostMapping("/login")
public String loginV2(@Valid @ModelAttribute LoginForm form, BindingResult bindingResult, HttpServletResponse response) {
if (bindingResult.hasErrors()) {
return "login/loginForm";
}
...
// 로그인 성공 처리
// 세션 관리자를 통해 세션을 생성하고, 회원 데이터 보관
sessionManager.createSession(loginMember, response);
return "redirect:/";
}
@PostMapping("/logout")
public String logoutV2(HttpServletRequest request) {
sessionManager.expire(request);
return "redirect:/";
}
위에서 세션 로직을 직접 만들어보았다.(SessionManager) 세션의 원리를 파악하기 위해 직접 만들어 보았지만 실제로는 스프링이 주는 session 기능을 이용하여 쉽게 설정할 수 있다.(위에서 내가 만든 것은 자동 만료 기능도 없...)
@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();
session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);
// 세션 관리자를 통해 세션을 생성하고, 회원 데이터 보관
// sessionManager.createSession(loginMember, response);
return "redirect:/";
}
HttpSession타입으로 HttpServletRequest에서 getSession 메서드를 통해 세션 객체를 가져올 수 있다. 이 객체를 통해 로그인 성공시 로그인 정보를 세션에 담아준다.(세션에 담는다 = 쿠키에 세션ID 전달,서버 세션 보관소에 세션ID 생성)
request.getSession(true)
디폴트로는 파라미터 create = true이다. 이는 세션이 존재하지 않을 때, session을 생성 한다는 것이다. 여기서 session을 생성한다는 것은 sessionId를 생성해서 브라우저 쿠키에 이를 구성한다는 것이며 세션이 존재하지 않는다는 것은 브라우저 쿠키에 sessionId가 존재하지 않는다는 것이다.
Session
무언가 아직도 session이란 말이 굉장히 추상적이며 모호하다. session은 쿠키를 활용하여 서버 측에서 관리되는 상태 유지 기술이다. 세션을 활용한다는 것은 곧 sessionId를 활용한다는 것이고
session이라는 영어를 해석했을 때 "기간"적 의미가 담겨있 듯 만료시간등 을 활용한(그래서 쿠키를 주로 이용)일시적인 사용권한 권리정도로 느끼면 좋을 것 같다.
//homeController
@GetMapping("/")
public String homeLoginV3Spring(@SessionAttribute(name = SessionConst.LOGIN_MEMBER, required = false) Member member,
Model model) {
if (member == null) {
return "home";
}
model.addAttribute("member", member);
return "loginHome";
}
request.getSession 대신 @SessionAttribute를 사용하여 request의 session에서 "loginMember"로 조회하여 value인 UUID값을 찾아 세션 보관소에서 이와 매칭되는 객체인 Member를 바로 주입받을 수 있다.

이렇게 로그인을 진행하면 스프링의 세션객체가 자동적으로 생성해준 유일한 값인 JSESSIONID값이 포함된다.
