로그인 방식에는 쿠키와 세션을 이용하는방식 JWT를 사용하는방식이 있다. JWT에 대한 글은 이 글을 보면된다.
도메인에서 중요한것은 도메인 영역에는 화면, UI, 기술 인프라 등등의 영역을 제외한 시스템이 구현해야하는 핵심 비즈니스 영역을 설정해야한다.
왜냐하면 web을 나중에 다른 기술로 바꾸어도 도메인은 그대로 유지 할 수 있어야한다.
이렇게 하려면 web은 domain을 알고 있지만 domain은 web을 모르도록 설계해야한다.
이것을 web은 domain을 의존하지만, domain은 web을 의존하지 않는다고 표현한다.
예를 들어 web 패키지를 모두 삭제해도 domain에는 전혀 영향이 없도록 의존 관게를 설계하는 것이 중요하다.
domain은 web을 참조해서는 안된다.
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<link th:href="@{/css/bootstrap.min.css}"
href="css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container" style="max-width: 600px">
<div class="py-5 text-center">
<h2>홈 화면</h2>
</div>
<div class="row">
<div
class="col">
<button class="w-100 btn btn-secondary btn-lg" type="button"
th:onclick="|location.href='@{/members/add}'|">
회원 가입
</button>
</div>
<div
class="col">
<button class="w-100 btn btn-dark btn-lg"
onclick="location.href='items.html'"
th:onclick="|location.href='@{/login}'|" type="button">
로그인
</button>
</div>
</div>
<hr class="my-4">
</div> <!-- /container -->
</body>
</html>
memberRepository
저장 공간은 map을 썻다. 그다음에 save메서드 findBy Id,finadAll과 같은 메서드를 만들었다.
사실 이걸 memberRepository를 인터페이스로 만들고 이걸 구현체를 MemberRepository로 만들었으면 이걸 지우고 나중에 다른 걸로 대체하면 그냥 오버라이딩만 하면 되서 좋긴한데,, 규모가 작으니 일단 이렇게 진행하겠다.
memberController
동일한 url이지만 하나는 get방식 하나는 post방식이다. get방식은 단순하게 회원가입 form을 반환하는것이고
save메서드는 Member를 저장해주는 함수이다. @valid애노테이션을 사용하여 오류가 있으면 bindingReuslt에 담았다. 그리고 만약 오류가 있으면 다시 addform으로 넘어가게 하였다.
회원가입 뷰 템플릿
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<link th:href="@{/css/bootstrap.min.css}"
href="../css/bootstrap.min.css" rel="stylesheet">
<style>
.container {
max-width: 560px;
}
.field-error {
border-color: #dc3545;
color: #dc3545;
}
</style>
</head>
<body>
<div class="container">
<div class="py-5 text-center">
<h2>회원 가입</h2>
</div>
<h4 class="mb-3">회원 정보 입력</h4>
<form action="" th:action th:object="${member}" method="post">
<div
th:if="${#fields.hasGlobalErrors()}">
<p
class="field-error" th:each="err : ${#fields.globalErrors()}"
th:text="${err}">전체 오류 메시지</p>
</div>
<div>
<label for="loginId">로그인 ID</label>
<input type="text" id="loginId" th:field="*{loginId}" class="form control" th:errorclass="field-error">
<div class="field-error" th:errors="*{loginId}" />
</div>
<div>
<label for="password">비밀번호</label>
<input type="password" id="password" th:field="*{password}" class="form-control" th:errorclass="field-error">
<div class="field-error" th:errors="*{password}" /></div>
<div>
<label for="name">이름</label>
<input type="text" id="name" th:field="*{name}" class="form-control" th:errorclass="field-error">
<div class="field-error" th:errors="*{name}" />
</div>
<hr
class="my-4">
<div
class="row">
<div
class="col">
<button
class="w-100 btn btn-primary btn-lg" type="submit">회원 가
입</button>
</div>
<div
class="col">
<button
class="w-100 btn btn-secondary btn-lg"
onclick="location.href='items.html'"
th:onclick="|location.href='@{/}'|"
type="button">취소</button>
</div>
</div>
</form>
</div> <!-- /container -->
</body>
</html>
회원용 테스트 데이터 추가
지금 mysql과 같은 다른 DB를 사용하는게 아니라 메모리에 올리는거라 실행을 중단하면 저장한 회원이 사라지므로 애초에 spring을 띄울때 test 데이터를 삽입하는 과정을 만들겠다.
PostConstruct로 인해 spring을 띄울때 리포지토리에 test객체를 저장하는것을 볼 수 있다.
LoginService
stream을 사용해서 아이디를 찾아서 동일하면 member객체를 반환하고 아니면 null을 return
Login Form
Login Controller
@Slf4j
@Controller
@RequiredArgsConstructor
public class LoginController {
private final LoginService loginService;
@GetMapping("/login")
public String loginForm(@ModelAttribute("loginForm") LoginForm form) {
return "login/loginForm";
}
@PostMapping("/login")
public String login(@Valid @ModelAttribute LoginForm form, BindingResult
bindingResult) {
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";
}
//로그인 성공 처리 TODO
return "redirect:/";
}
}
LoginForm메서드는 그냥 LoginForm을 반환하는것이다. 다만 클라이언트 측이 LoginForm객체에다가 username password를 보내서 바인딩 되어서 modelAttribute로 model에 담아서 넘긴다.
login에서는 검증부분이 있는데 첫번째는 바인딩 자체가 안된거다. 그래서 bindingResult.hasError()즉 정상적인 필드값으로 입력을 안했으면 다시 loginForm으로 보낸다.
이게 이전편에서 설명을 했는데 ModelAttribute는 프로퍼티 접근법으로 접근하므로, 오류가있는것 오류가 없는것 따로 접근이 가능해서 Valiated 애노테이션을 사용가능하지만, RequestBody는 애초에 바인딩이 안되면 그냥 호출자체가 안됨
쨋든, 그리고 loginService에서 login메서드에 id와 passsowrd를 인자로 보내고 만약 id password가 틀려서 loginMember가 null이면 vindingReuslt에 글로벌 오류로 "loginFail"이라는 error code로 보낸다.
로그인 성공시는 그다음 할것이므로 일단 넘긴다.
로그인 폼 뷰 템플릿
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<link th:href="@{/css/bootstrap.min.css}"
href="../css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container" style="max-width: 600px">
<div class="py-5 text-center">
<h2>홈 화면</h2>
</div>
<h4 class="mb-3" th:text="|로그인: ${member.name}|">로그인 사용자 이름</h4>
<hr class="my-4">
<div class="row">
<div
class="col">
<button class="w-100 btn btn-secondary btn-lg" type="button"
th:onclick="|location.href='@{/items}'|">
상품 관리
</button>
</div>
<div
class="col">
<form
th:action="@{/logout}" method="post">
<button class="w-100 btn btn-dark btn-lg" type="submit">
로그아웃
</button>
</form>
</div>
</div>
<hr class="my-4">
</div> <!-- /container -->
</body>
</html>
로그인 상태를 어떻게 유지할 수 있을까?
서버에서 로그인에 성공하면 HTTP응답에 쿠키를 담아서 브라우저에 전달한다. 그리고 브라우저는 앞으로 해당 쿠키를 지속해서 보내준다.
쿠키생성
클라이언트 쿠키전달1
클라이언트 쿠키전달2
쿠키의 종류
LoginController-login()
이전에 Login성공시 못했던 Todo로직을 짜보자.
@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());
log.info("login? {}", loginMember);
if (loginMember == null) {
bindingResult.reject("loginFail", "아이디 또는 비밀번호가 맞지 않습니다.");
}
return "login/loginForm";
//로그인 성공 처리
//쿠키에 시간 정보를 주지 않으면 세션 쿠키(브라우저 종료시 모두 종료)
Cookie idCookie = new Cookie("memberId",
String.valueOf(loginMember.getId()));
response.addCookie(idCookie);
return "redirect:/";
}
로그인 성공 처리부분을 작성하였다.
new Cookie로 쿠키를 만드는데 시간정보를 주지 않으므로 세션 쿠키가 발급된다.
여기서 일단 Cookie는 String값이 value로 들어가므로 Member의 id는 Long이므로 캐스팅을 해주고, addCookie메서드를 호출한뒤에 홈으로 redirect한다.
홈-로그인처림
@Slf4j
@Controller
@RequiredArgsConstructor
public class HomeController {
private final MemberRepository memberRepository;
@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";
}
}
홈(/)요청이 들어왔을때 파라미터에서 CookieValue로 MemberIdㄹ르 가져온다. 일단 로그인이 안되어있을수 도 있으니까 required를 false로 놓고, CookieValue에서 자동적으로 String을 Long으로 캐스팅해준다. 그래서 여기서는 Long을 사용해도된다.
로그인 로직에서 findById로 Member를 찾고 있으면 model에 그 객체를 담아서 loginHome view로 가고 없으면
로그인에 실패했으므로 다시 home으로 간다.
홈-로그인 사용자 전용
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<link th:href="@{/css/bootstrap.min.css}"
href="../css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container" style="max-width: 600px">
<div class="py-5 text-center">
<h2>홈 화면</h2>
</div>
<h4 class="mb-3" th:text="|로그인: ${member.name}|">로그인 사용자 이름</h4>
<hr class="my-4">
<div class="row">
<div
class="col">
<button class="w-100 btn btn-secondary btn-lg" type="button"
th:onclick="|location.href='@{/items}'|">
상품 관리
</button>
</div>
<div
class="col">
<form
th:action="@{/logout}" method="post">
<button class="w-100 btn btn-dark btn-lg" type="submit">
로그아웃
</button>
</form>
</div>
</div>
<hr class="my-4">
</div> <!-- /container -->
</body>
</html>
로그아웃기능
세션 쿠키이므로 웹브라우저 종료시 서버에서 해당 쿠키의 종료 날짜를 0으로 지정한다.
LoginController - logout 기능 추가
@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);
}
만약 logout url로 요청이 들어오면 exprieCookie메서드를 사용한다.
아까 memberId로 value값을 설정했으므로 이번에는 value가 null인 쿠키를 만들어서 Age를 0으로 만들어주면 된다.
근데 이 쿠키의 문제가 뭐냐면 쿠키값을 임의로 변경할 수 있다는것이다.
클라이언트가 쿠키를 만약 강제로 변경하면 다른 사용자가 된다.
그러니까 Cookie: "memberId":1에서 id를 2로 바꿔서 요청을 보내면 다른 사용자의 이름이 보는것이다.
또한 쿠키에 보관된 정보는 훔쳐갈 수 있다.
만약 쿠키에 개인정보나, 신용카드 정보가 있다면 문제가 될것이다.
또한, 쿠키가 한번 탈취당하면 만료될때까지 계속 사용할 수 있다.
따라서, 쿠키에 중요한 값을 노출시키지 않기위해서 이 id값을 임의의 랜던 값으로 바꾸고, 서버에서 토큰과 사용자 id를 매핑해서 인식해야한다. => 이렇게 되면 서버측에서 토큰을 관리하는것이다.
또한, 토큰은 해커가 임의의 값을 넣어도 찾을 수 없도록 예상 불가능하게 해야한다.
해커가 토큰을 털어가도 시간이 지나면 사용할 수 없도록 토큰의 만료시간을 짧게 유지하고, 해킹이 의심되는 경우 서버에서 해당 토큰을 강제로 제거해야한다.
로그인 처리하기 - 세션 동작방식
로그인시
세션저장소를 따로 만든다. 그래서 loginId와 password를 전달받으면 해당 사용자가 맞는지 확인후에
uuid를 사용하여서 세션 id를 생성해서 세션저장소에 보관한다.
이제는 memberId값을 가지고 접근하는게 아니라 세션Id를 쿠키로 전달하여서 이제는 세션id로 접근을 하게 된다.
여기서 중요한 점은 회원과 관련된정보는 전혀 클라이언트에 전달하지 않는다는 점이다.
오직 추정 불가능한 세션 ID만 쿠키를 통해 클라이언트에 전달한다.
로그인 이후 접근할때는 쿠키저장소에 저장된 sessionId로 접근하면 된다.
클라이언트는 요청시 항상 mySessionId쿠키를 전달한다.
서버에서는 클라이언트가 전달한 mySessionId 쿠키 정보로 세션 저장소를 조회해서 로그인시 보관한 세션정보를 사용한다.
이렇게 하면 좋은점
이번에는 세션을 직접 만들어서 적용해볼것이다.
세션관리
@Component
public class SessionManager {
public static final String SESSION_COOKIE_NAME ="mySessionId";
private Map<String,Object> sessionStore = new ConcurrentHashMap<>();
//세션 생성
//여기서 value=>member1
public void createSession(Object value, HttpServletResponse response){
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());
}
}
private Cookie findCookie(HttpServletRequest request, String cookieName) {
if(request.getCookies()==null){
return null;
}
//request.getCookies면 뭐 지금은 member가 cookie에 저장되어있을수 있으나 session_cookie_name,123-2332-32432, myage,10 이런식으로
//쿠키가 여러개 저장되어있는경우 Session_cookie_name으로 저장된 그 쿠키 session_cookie_name,123-2332-32432 이걸 가져오기위한 로직
//쿠기의 key값이 session_cookie_name인게 있으면 그러면 그 쿠키를 반환
return Arrays.stream(request.getCookies())
.filter(cookie -> cookie.getName().equals(cookieName))
.findAny()
.orElse(null);
}
}
세션 생성
sessionId를 UUID를 통해 만들고 sessionStore에 sessionId와 해당하는 Object value를 넣는다.
그다음에 쿠키를 생성해서 response.add로 보내준다.
세션 조회
findCooki메서드로 sessionId에 해당하는 쿠키를 찾는다.
그다음에 sessionStore에서 get메서드로 해당하는 value를 가져와 반환한다.
세션 만료
sessionId로 쿠키를 찾은 다음에 value를 가져와서 remove메서드로 ConcurrentHashMap에서 지워준다.
findCookie메서드
일단 request에서 cookie를 가져오려면 쿠키가 배열로 반환해주므로, 이걸 stream을 돌려서 빼내야한다. cookie.getName으로 이름이 cookieName과 동일하다면 반환하고 없으면 null을 반환한다.
TestCode
public 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();
}
}
여기서 Mock라는게 나오는데 일단 우리는 HttpServletRequest에서 쿠키를가져오고 response에다가 쿠키를 담아서 전달해야하는데
HttpServletRequest자체가 인터페이스이다.
그래서 구현체가 없으니까 이걸 Mock를 이용해서 만들어서 사용한다.
Mock가 이 서블릿 리퀘스트를 구현하고 있다.
어쨋든,
MockHttpServletResponse를 생성해서 Member를 만들어서 넣어주고,
클라이언트에서 서버로 요청을 보낼때 이 response의 cookie를 get해서 넣어준다.
세션조회할때는 sessionmanager에서 구현해놓은 getSession메서드를 활용하면된다.
만료는 expire메서드를 호출하면된다.
LoginController - 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:/";
}
로그인 성공 처리 부분에서
sessionManager.createSession메서드에 우리가 login인한 Member를 인자로 넣어준다.
세션에 loginMember를 저장하고 쿠키도 함께 발행하는것이다.
logoutV2
@PostMapping("/logout")
public String logoutV2(HttpServletRequest request) {
sessionManager.expire(request);
return "redirect:/";
}
request에 있는 쿠키를 지우기 위해서 expire메서드의 request를 인자로 전달한다.
로그아웃시 해당 세션의 정보를 제거한다.
homeLoginV2
@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";
홈으로 오면 일단 getSession메서드를 호출하여서 세션에 있는 쿠키를 조회해서 Member가 있는지 확인한다. member가 null이면 로그인을하여 쿠키를 발급받지 않았다는 뜻이므로 로그인을 다시 해야하니까 home으로 이동한다.
그게 아니라 member가 있다면 model에다가 해당 member를 담아주고 loginHome 폼으로 이동한다.
정리
세션과 쿠키의 개념을 이해하기 위해서 직접 만들어 보았는데 사실, 세션이라는것이 뭔가 특별한 것이아니라 단지 쿠키를 사용하는데 서버에서 데이터를 유지하는 방법임을 이해할 수 있다.
그러나 서블릿도 세션 개념을 지원하고 우리는 서블릿이 제공하는 세션을 사용하면 된다.
참고.
쿠키와 세션을 헷갈려하지 말고,
쿠키는 -> 클라이언트측에서 key(sessionID),value로 값을 보관해 놓는것이고
세션은 -> 쿠키를 기반으로 sessionId에 해당하는 value를 서버측에서 저장하고 관리하는것이다.
서블릿은 세션을 위해 HttpSession이라는 기능을 제공한다.
서블릿이 제공하는 HttpSession도 결국 우리가 만든 SessionManager와 동일한 방식으로 작동하는데, HttpSession을 생성하면 JSESSIONID라는 이름의 쿠키를 생성하고 값은 추정불가능한 랜덤값으로 생성한다.
일단 HttpSession에 데이터를 보관하고 조회할때 이름이 중복되어서 사용되므로 상수하나를 정의했다.
loginV3
@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());
log.info("login? {}", loginMember);
if (loginMember == null) {
bindingResult.reject("loginFail", "아이디 또는 비밀번호가 맞지 않습니다.");
return "login/loginForm";
}
//로그인 성공 처리
//세션이 있으면 있는 세션 반환, 없으면 신규 세션 생성
HttpSession session = request.getSession();
//세션에 로그인 회원 정보 보관
session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);
return "redirect:/";
}
로그인 성공 처리부분에서 request.getSession으로 세션을
생성하고 session의 setAttribute에서 key를 앞에서 만든loginMember,폼을 해당하는 Member로 넘겨주었다.
그러므로, session.setAttribute를 하면
세션 저장소에,
JSESSIONID 랜덤값: MAP< sessionConst.LOGIN_MEMBER,loginMember>
이런식으로 저장된다.
그러면 우리는 이 랜덤값으로 MAP으로 되어있는 한쌍의 key,value를 확인할 수 있는거다.
logoutV3
@PostMapping("/logout")
public String logoutV3(HttpServletRequest request) {
//세션을 삭제한다.
HttpSession session = request.getSession(false);
if (session != null) {
session.invalidate();
}
return "redirect:/";
}
session이 null이아니라 있으면 그 세션을 invalidate메서드로 전부 지운다.
request.getSession이면 Member와 관련되지 않은 여러 세션들이 존재 할 수 있다. 예를들어 Member가 아니라 다른 session을 setAttribute로 생성해서 만들어놨다면
request.getSession을 하면 이 session에는 Member뿐만아니라 다른 세션도 들어있다.
하지만 logout을 하면 기존 사용자의 정보를 다 지우는게 맞으므로 invalidate메서드를 통해서 모든 세션을 삭제한다.
homeLoginV3
@GetMapping("/")
public String homeLoginV3(HttpServletRequest request, Model model) {
//세션이 없으면 home
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";
}
세션을 get해서 없으면 home으로 보내고
있으면 key에해당하는 SessionCOnst.Login_Member에서 Member로 캐스팅해서 가져온다.
만약 세션에 회원 데이터가 없으면 home으로 보내고 유지되면 model에 담는다.
이게 왜 session==null과 loginMember == null로 나눠놨냐면
session == null에서는 애초에 session자체가 아에 없을수 있다.
그래서 getSession했을때 없으면 null을 반환하니까 null로 검사하고,
일단 Session이 있어서 왔는데 이게 Member에 대한 session이 아닐 수도 있다. 그래서 SessionConst.Login_Meber로 해당하는 Value를 찾았는데 있으면 가져오고 없으면 NULL을 반환하므로 확인한다.
그러면 우리는 클라이언트가 request에서 쿠키를 넣어서 요청하는 과정은 안해줬는데 이런걸 HTTPSession이 애초에 쿠키를 넣어서 요청을 보내준다.
@SessionAttribute
SessionAttriubte 에노테이션을 사용하면 이전에는
//세션이 없으면 home
HttpSession session = request.getSession(false);
if (session == null) {
return "home";
}
Member loginMember = (Member)
session.getAttribute(SessionConst.LOGIN_MEMBER);
세션을 가져와서 "loginMember"에 해당하는 Value인 Member를 가져오는 과정을
거쳤는데
이제는,
@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";
이렇게 하면 세션을 가져와서 세션에 들어있는 데이터를 찾는 과정을 스프링이 한번에 처리해준다.
TrackingModes
로그인을 처음 시도하면 jessionid를 url에 포함하고 있음을 알 수 있다.
http://localhost:8080/;jsessionid=F59911518B921DF62D09F0DF8F83F872
이렇게 하는 이유가 뭐냐면, 웹 브라우저가 쿠키를 지원하지 않는경우 URL을 통해서 세션을 유지하는 방법이다.
서버측에서 웹 브라우저가 쿠키를 지원할지 안할지 모르니까, 일단 최초에는 쿠키값도 전달하고, URL에 JessionId도 함께 전달하는것이다.
URL 전달 방식을 끄고, 항상 쿠키를 통해서만 세션을 유지하고 싶으면 아래 옵션을 넣어주면 된다.
application.properties
server.servlet.session.tracking-modes=cookie
Session을 사용한 로그인 기능에서는 SessionAttribute를 사용하면 된다.
그리고 name으로 찾고자하는 Value의 Key로 설정해주면된다.
세션 타임아웃 설정
사용자가 로그아웃 버튼을 눌러서, session.invalidate()가 호출되면 세션이 삭제가 된다. 그런데 대부분 사용자는 로그아웃을 누르지 않고 브라우저를 종료해버리니까, HTTP가 ConnectionLiess상태이므로 서버 입장에서는 해당 사용자가 웹브라우저를 종료했는지 안했는지 알 수 가 없다.
만약 남아있는 세션을 무한정 보관한다면,
application.properties에다가
server.servlet.session.timeout=1800(30분)
으로 설정하면
만약 마지막 요청으로 부터 세션사용시간이 30분이 지나면 was가 내부에서 해당 세션을 제거한다.
그게아니라 요청을 30분내에 다시하면 수명이 30분 연장된다.
중요한건, 서버측의 session storage에 지금 예제처럼 key: login_ID, value: Member 이런식으로 넣어놓으면 안된다. 왜냐하면 예제에서는 이 스토리지가 메모리에 생성되므로, 보관한 데이터 용량 * 사용자수로 세션의 메모리 사용량이 엄청나게 늘어나서 장애가 발생 할 수 있다.
또한 세션의 시간을 너무 길게 가져가면 메모리 사용량이 누적 될 수 있으므로 적당한 시간을 선택하는것이 중요한다.
다음시간에 할것
다음시간부터는 필터랑 인터셉터를 배울껀데, 지금 현재
localhost:8080/items로 get요청하면 로그인 유무와 관계없이 접근이 가능하다. 왜냐하면 우리가 로그인 하지 않고 요청을 할시 접근을 제한하는 필터를 안 만들었기 때문이다.