로그인의 상태를 어떻게 유지할 수 있을까?
쿼리 파라미터를 계속 유지하는 것은 어렵고 번거롭다.
쿠키를 사용해보자
서버에서 로그인에 성공하면 HTTP 응답에 쿠키를 담아서 브라우저에 전달하기
그러면 브라우저는 앞으로 해당 쿠키를 지속해서 요청마다 보내준다.
모든 요청에 쿠키 정보 자동으로 포함된다.!
쿠키에는 영속 쿠키와 세션 쿠키가 있다.
@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:/";
}
로그인에 성공하면 쿠키를 생성하고 HttpServletResponse에 담는다. 쿠키 이름은 memberId이고, 값은 회원의 id를 담아둔다. 웹 브라우저는 종료 전까지 회원의 id를 서버에 계속 보내줄 거임.
로그인이 성공하면 로그인 한 사용자 전용 홈 화면 만들어 주기
public class HomeController {
private final MemberRepository memberRepository;
// @GetMapping("/")
public String home() {
return "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";
}
}
로그아웃 기능 만들기
@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);
}
문제 1: 쿠키 값은 임의로 변경할 수 있다.
문제 2: 쿠키에 보관된 정보는 훔쳐갈 수 있다.
문제 3: 해커가 쿠키를 한번 훔쳐가면 평생 사용할 수 있다.
클라이언트와 서버는 결국 쿠키로 연결이 되어야 한다.
- 서버는 클라이언트에 mySessionId라는 이름으로 세션ID 만 쿠키에 담아서 전달한다.
- 클라이언트는 쿠키 저장소에 mySessionId 쿠키를 보관한다.
중요
회원과 관련된 정보는 전혀 클라이언트에 전달하지 않는다.
오직 추정 불가능한 세션 ID만 쿠키를 통해 클라이언트에 전달한다.
@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) {
//세션 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);
}
}
주요 로직은
1.createSession -> 세션 만든다.
java.util.UUID를 활용해서 sessionId 생성해줌
sessionStore에 id와 member객체 넣고
Cookie 생성 후 response에 담아주면 끝
2.getSession -> 세션 얻기
findCookie로 request에 쿠키가 담겼는지 일단 확인
sessionCookie 가 null 이면 그냥 널 반환
이후 sessionStore 즉 서버쪽 세션 저장소에서 쿠키 찾고
잇으면 맴버 객체 반환 없으면 null 반환
3.expire -> 세션 만료 시키기
V2 임
@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());
log.info("login? {}", loginMember);
if (loginMember == null) {
bindingResult.reject("loginFail", "아이디 또는 비밀번호가 맞지 않습니다.");
return "login/loginForm";
}
//로그인 성공 처리
//세션 관리자를 통해 세션을 생성하고, 회원 데이터 보관
sessionManager.createSession(loginMember, response);
return "redirect:/";
}
v2 로그아웃
@PostMapping("/logout")
public String logoutV2(HttpServletRequest request) {
sessionManager.expire(request);
return "redirect:/";
}
v2 홈화면 처리
@GetMapping("/")
public String homeLoginV2(HttpServletRequest request, Model model) {
//세션 관리자에 저장된 회원 정보 조회
Member member = (Member)sessionManager.getSession(request);
if (member == null) {
return "home";
}
//로그인
model.addAttribute("member", member);
return "loginHome";
}
v3 로 바로 살펴보자
@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);
return "redirect:/";
}
세션 생성과 조회
request.getSession(true)
로그아웃 v3
@PostMapping("/logout")
public String logoutV3(HttpServletRequest request) {
HttpSession session = request.getSession(false);
if (session != null) {
session.invalidate();
}
return "redirect:/";
}
request.getSession(false)가 쓰엿다
session.invalidate() 서버쪽 세션 제거함
@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";
}
server.servlet.session.tracking-modes=cookie
이거 넣어주기
세션은 사용자가 로그아웃을 직접 호출해서 session.invalidate() 가 호출 되는 경우에 삭제된다.
그런데 대부분의 사용자는 로그아웃을 선택하지 않고, 그냥 웹 브라우저를 종료한다. 문제는 HTTP가 비
연결성(ConnectionLess)이므로 서버 입장에서는 해당 사용자가 웹 브라우저를 종료한 것인지 아닌지를
인식할 수 없다. 따라서 서버에서 세션 데이터를 언제 삭제해야 하는지 판단하기가 어렵다.