728x90
1. domain
- 도메인
- 화면, UI, 기술 인프라 등등의 영역을 제외한 시스템이 구현해야 하는 핵심 비즈니스 업무 영역
- controller, Service, Repository에서 사용하는 data
- 향후 web을 다른 기술로 바꾸어도 도메인은 그대로 유지할 수 있어야 한다.
- web은 domain에 의존하지만 domain은 web에 의존하지 않게 설계해야한다.
- web 패키지를 모두 제거해도 domain에는 전혀 영향이 없도록 의존관계를 설계하는 것이 중요
== domain은 web을 참조하면 안된다.
- domain: data, service 로직
- web: controller, form 관리하는 로직
2. test data
package hello.login;
@Component
@RequiredArgsConstructor
public class TestDataInit {
private final ItemRepository itemRepository;
private final MemberRepository memberRepository;
/**
* 테스트용 데이터 추가
*/
@PostConstruct
public void init() {
itemRepository.save(new Item("itemA", 10000, 10));
itemRepository.save(new Item("itemB", 20000, 20));
Member member = new Member();
member.setLoginId("test");
member.setPassword("test!");
member.setName("테스터");
memberRepository.save(member);
}
}
3. 로그인 기능
- domain-service
- web-form과 controller
// domain.login/LoginService
@Service
@RequiredArgsConstructor
public class LoginService {
private final MemeberRepository memberRepository;
public Member login(String loginId, String password){
// Stream 사용가능한 이유는 memberRepository.findByLoginId() return value가 Optional이기 때문
return memberRepository.findByLoginId(loginId)
.filter(m -> m.getPassword().equals(password))
.orElse(null);
}
}
//web.login/LoginForm;
@Data
public class LoginForm{
@NotEmpty
private String loginId;
@NotEmpty
private String password;
}
// web.login/LoginController
@Slf4j
@Controller
@RequiredArgsConstructor
public class LoginController {
private final LoginService loginService;
@GetMapping("/login")
public String loginFrom(@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.getPssword());
log.info("login? {}", loginMember);
// global 오류 valitdation은 직접 작성
if (loginMember == null){
bindingResult.reject("loginFail", "id, pw 일치하지 않습니다");
return "login/loginForm";
}
// 로그인 성곳 처리 todo
return "redirect:/";
}
}
4. 로그인 처리하기 - 쿠키 사용
4.1. 쿠키
- 쿠키
- login이 성공 시 server는 http 응답에 쿠키를 담아서 browser에 전달
- 이제 browser는 http 요청시마다 쿠키를 요청에 넣어서 서버에 전달
- 쿠키 종류
- 영속쿠키: 만료 날짜를 입력시 해당 날짜까지 유지
- 세션쿠키: 만료 날짜 생략시 브라우저 종료시 까지만 유지
4.2. 쿠키 생성
- loginController 세션 쿠키 생성
- 만약 영속 쿠키 생성을 원하면 초기화 값에 날짜를 넣어주면 된다.
- 로그인 성공시 cookie를 생성하고 HttpServletResponse에 담는다.
- new Cookie("memberId", String.valueOf(loginMember.getId()));
- 쿠키의 이름은 "memberId", 값은 회원의 id(=getId()값)를 담는다.
public String login(@Valid @ModelAttribute LoginForm form, BindingResult bindingResult, HttpServletResponse response) {
if (bindingResult.hasErrors()) {
return "login/loginForm";
}
Member loginMember = longinService.login(form.getLoginId(), form.getPassword());
if (loginMember == null){
bindingResult.reject("loginFail", "id or pw 맞지 않습니다");
return "login/loginForm";
}
// login 성공 처리 todo
// 세션 쿠키 생성
// 쿠키에 시간 정보를 주지 않으면 세션 쿠키(브라우저 종료시 모두 종료)
Cookie idCookie = new Cookie("memberId", String.valueOf(loginMember.getId()));
response.addCookie(idCookie); // http 응답에 response를 보낸다.
return "redirect:/"
4.3. 쿠키 홈화면
@GetMapping("/")
public String homeLogin(@CookieValue(name = "memberId", required = false) Long memberId, Moedl model){
// 1. login 안된 상황
if(memberId == null) return "home";
// cookie가 존재해서 Long memberId가 존재하는 상황
// cookie의 문제점 : 실제 repository를 건들인다.
Member loginMember = memberRepsoitory.findById(memberId);
// 2. 쿠키는 존재하지만 실제 login 정보가 없는 경우
if(loginMember == null) return "home";
// 3. 쿠키와 그에 맞는 data가 있는 경우
model.addAttribute("member", loginMember);
return "loginHome";
}
- loginController에서 login 성공 처리 todo에서 홈으로 redirect한다.
- 홈 controller 수정
- 홈 controller는 원래 사이트 들어갈 때 첫 화면을 나타내주는 controller이다.
- 3가지 상황을 고려해서 수정 할 것이다.
- 홈 controller는 로그인하지 않은 사람들도 봐야한다. -> home.html 화면을 보여준다.
- cookie 중에서도 오래되서 쿠키는 존재하는데 실제 repository에 값이 없는 경우 -> home.html 화면을 보여준다.
- cookie 중에서도 실제 repository에 값이 있는 경우 -> loginhome.html 화면을 보여준다.
- 코드 부연 설명
- @CookieValue(name = "memberId", required = false) Long memberId, Model model)
- @CookieValue() : Cookie를 받는 방법 중 Spring 기능
- required = false : login 안한 사람들도 해당 "home"으로 들어갈수있어야하기 때문
- Long memberId : Cookie의 값으로 저장한 id의 값은 String이였다. 하지만 Spring의 type converting으로 type을 변경
5. 로그아웃 - 쿠키
- html
// html
// 해당 button을 누르면 form이 작동해서 Post로 보낸다.
<form th:action="@{/logout}" method="post">
<button class="w-100 btn btn-dark btn-lg" type="submit">
로그아웃
</button>
</form>
- loginController
- 새로운 cookie를 만들어서 시간을 0으로 셋팅 후 browser에게 보내는 원리
@PostMapping("/logout")
public String logout(HttpServletResponse response) {
expireCookie(response, "memberId");
return "redirext:/";
}
private void expireCookie(HttpServletResponse response, String cookieName){
// 기존의 cookie와 동일한 이름의 cookie를 생성 후 덮어쓸것이다.
Cookie cookie = new Cookie(cookieName, null);
cookie.setMaxAge(0); // cookie 시간 설정
response.addCookie(cookie); // 새로 쓴 쿠키 보내주기
}
개념
- 사용자가 보낸 쿠키를 사용하지 않고 새로 쿠키를 생성하는 이유, 기존의 쿠키는 어떻게 되는가?(삭제되는지, 수정되는지)
- 사용자의 브라우저에 저장된 쿠키를 만료시켜야 하기 때문
사용자가 서버로 보낸 쿠키는 사용자의 브라우저에 저장된 쿠키의 복사본이다.
따라서 사용자의 브라우저에 저장된 쿠키(member id)를 만료시키려면 서버에서 동일한 이름(member id)으로 쿠키를 만들고 - 브라우저에 저장된 쿠키를 새로운 쿠키로 덮어쓰게 만들어야 한다.
- 사용자의 브라우저에 저장된 쿠키를 만료시켜야 하기 때문
- 서버에서 새롭게 쿠키를 만들어 사용자에게 전달하는 방법 외에 사용자의 브라우저에 저장된 쿠키를 제어하는 방법은 없다.
- logout도 응답 쿠키를 생성 : Max-Age=0을 확인할 수 있다.
6. 쿠키와 보안 문제
- 쿠키를 사용한 로그인 Id의 보안 문제들
- 쿠키 값은 임의로 변경이 가능 -> 나의 쿠키를 보고 다른 사람의 쿠키도 예측이 가능해진다.
- 쿠키에 보관된 정보는 훔쳐갈 수 있다. -> 쿠키 정보가 나의 로컬 PC에서 털릴 수 있고 네트워크 전송 구간에서 털릴 수 있다.
- 해커가 쿠키를 한번 훔치면 평생 사용이 가능하다.
- 쿠키는 쿠키 정보를 이용해서 db의 실제 값을 이용해서 응답에 사용하는데, db를 직접 건들이기 때문에 쿠키가 털렸다고 db 정보를 함부로 건들일 수 없다.
- 대안
- 쿠키에 중요한 값을 노출하지 않고 예측불가능한 토큰(랜덤 값)을 노출하고 서버에서 토큰과 사용자 id를 매핑해서 인식
- 토큰은 서버에서 관리하도록
- 토큰은 해커가 임의의 값을 넣어도 찾을 수 없도록 예상 불가해야한다.
- 토큰을 해커가 털어도 만료시간을 짧게 유지한다.
- 대안 결론
- 세션을 이용
7. 로그인 처리하기 - 세션 동작 방식
- 쿠키
- 노란색 상자속 쿠키를 보게되면 1번이 memberId라는 것이 너무 가독성이 좋다
- 1 이란 쿠키 값이 어디서 어떻게 사용되는 것인지 확실히 알 수 있다.
- memberId로 표시 안할려고 하니 코드 내부적으로 가독성이 너무 떨어지는 안좋은 방향으로 흘러가게 된다.
- 쿠키와 실제 회원저장소와 직접적으로 연결이 되어있어서 해킹되었을 때 실제 db에 영향을 주게 된다.
- 세션
- 세션을 털어도 가독성이 떨어진다. 그저 mySessionId로만 되어있어서 어떤 위치의 data인지 알 수가 없다.
- 또한 어떤 data인지 알더라도 다른 사람들의 sessionId를 예상 못한다.
- session 시간을 정했기 때문에 sessionId 삭제되면 해킹에 진도에 차질이 생긴다.
- 첫 로그인 이후로는 실제 db가 아닌 임시로 생성한 세션저장소(복사본이 저장되어있다.)에서부터 db에서 받아와야하는 값을 받아오기 때문에 해킹이되더라도 실제 db는 보호받을 수 있다. 세션 저장소의 entity만 얼른 제거하던가 세션을 만료시키면 조금이나마 방어가 가능하다.
- 세션 작동 방식
- 쿠키와 전반적인 작동원리는 전반적으로 동일 - 차이점 : 세션 저장소, 세션 아이디(UUID)가 존재한다는 차이점
- 먼저 세션 저장소(table 형태)를 생성
- 로그인이 성공한 후 서버에서 쿠키를 넘겨줄 때
- 먼저 data를 db에 저장한다.
- 세션 저장소의 {key:value} 에 저장 - db repository를 사용하지 않고 세션 저장소를 이용한다.
- key : 난수로 되어있는 sessionId
- value: db에서 가지고 온 data
- session 저장소의 key값을 cookie에 담아서 보낸다.
cookie또한 key-value로 구성되어있다.- cookie key : "mySessionId" (그냥 별의미 없는 이름)
- cookie value : sessionId(난수)
- 먼저 data를 db에 저장한다.
- 참고
- Set-Cookie:는 지정한 이름이 아니라 request header의 Accept, Cache, 등등 목록의 이름이라고 생각하면된다.
- -> 브라우저에서 봤을 때 Cookie라고 되어있었다. Set-Cookie = Cookie란 동일하다.
- 쿠키와 전반적인 작동원리는 전반적으로 동일 - 차이점 : 세션 저장소, 세션 아이디(UUID)가 존재한다는 차이점
- 정리
- 쿠키 값 변조 -> 얘상 불가한 복잡한 sessionId 사용으로 예측 불가 시킴
- 쿠키 보관하는 정보는 클라이언트 해킹시 털릴 가능성이 존재 -> 세션Id가 털려도 중요 정보를 가지고 있지 않다. 또한 이 data 자체로는 어떤 data인지 추정이 불가하다.
- 쿠키 탈취 후 사용 -> 시간이 지나면 사용할 수 없도록 서버에서 세션의 만료시간을 짧게 유지 하거나 해당 세션을 강제로 제거 -> data를 막 제거할 수 있는 이유는 실제 db면 데이터 제거가 힘들지만 session저장소의 data는 복제된 값이므로 막 삭제해도 상관 없다.
- 쿠키 값 변조 -> 얘상 불가한 복잡한 sessionId 사용으로 예측 불가 시킴
8. 로그인 처리하기 - 세션 직접 만들기
- 3가지의 기능 제공
- 세션 생성
- sessionId 생성 (임의의 추정 불가능한 랜덤 값)
- 세션 저장소에 sessionId와 보관할 값 저장
- sessionId로 응답 쿠키를 생성해서 클라이언트에 전달
- 세션 조회
- 클라이언트가 요청한 session 쿠키의 값으로, 세션 저장소에 보관한 값 조회
- 세션 만료
- 클라이언트가 요청한 sessionId 쿠키의 값으로, 세션 저장소에 보관한 sessionId와 값 제거
- 세션 생성
- SessionManger 만들기
// SessionManger - web에 존재
@Component
public class SessionManger{
// 세션 저장소와 cookie에 들어갈 cookie 이름 지정
public static final String SESSION_COOKIE_NAME = "mySessionId"; // 1. Cookie를 보낼 때 이름 지정
private Map<String, Object> sessionStore = new ConcurrentHashMap<>();
/** 세션 생성 */
public void createSession(Object value, HttpServletResponse response){
// 세션 id를 생성하고, 값을 세션에 저장
String sessionId = UUID.randomUUID().toString(); // 2. Cookie를 보낼 때 이름의 대한 값을 지정
sessionStore.put(sessionId, value);
// 쿠키 생성
Cookie mySessionCookie = new Cookie(SESSION_COOKIE,sessionId); // 3. 1.2. 가 순차적으로 쿠키에 들어감
response.addCookie(mySessionCookie) // 4. 내가 만든 쿠키를 응답에 넣음
}
/** 세션 조회 */
public Object getSession(HttpServletRequest request){
Cookie sessionCookie = findCookie(request, SESSION_COOKIE_NAME); // {mySessionId, UUID} 가져옴
if (sessionCookie == null){
return null;
}
return sessionStore.get(sessionCookie.getValue());
// UUID 만 getValue로 꺼내고 UUID로 sessionStore에서 value를 반환
}
/** 세션 만료 */
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;
return Arrays.stream(request.getCookies()).filter(cookie -> cookie.getName().equals(cookieName).findAny().orElse(null);)
}
}
9. 로그인 처리하기 - 직접 만든 세션 적용
- LoginController - login
@PostMapping("/login")
public String loginV2(@Valid @ModelAttribute LoginForm form, BindingResult bindingResult, HttpServletResponse response) {
if (bindingResult.hasErrors()) {
return "login/loginForm";
}
Member loginMember = longinService.login(form.getLoginId(), form.getPassword());
if (loginMember == null){
bindingResult.reject("loginFail", "id or pw 맞지 않습니다");
return "login/loginForm";
}
// 로그인 성공 처리 TODO
// 세션 관리자를 통해 세션을 생성하고, 회원 데이터 보관
sessionManger.createSession(loginMember, response);
return "redirect:/";
}
- LoginController - logout
public String logoutV2(HttpServletRequest request){
sessionManger.expire(request);
return "redirect:/";
}
- HomeController
// @GetMapping("/")
public String homeLogin(@CookieValue(name = "memberId", required = false) Long memberId, Model model) {
// login 안된 상황
if (memberId == null){
return "home";
}
// login 된 상황
Member loginMember = memberRepository.findById(memberId);
if (loginMember == null) {
return "home";
}
model.addAttribute("member", loginMember);
return "loginHome";
}
/** 비교용도 - 위에가 cookie로 한것 아래가 session으로 한 것 */
// @GetMapping("/")
public String homeLoginV2(HttpServletRequest request, Model model) {
// 세션 관리자에 저장된 정보 조회
Member member = (Member)sessionManger.getSession(request);
// login 된 상황
if (member == null) {
return "home";
}
model.addAttribute("member", member);
return "loginHome";
}
- 세션매니저 test
package hello.login.web.session;
class SessionMangerTest {
SessionManger sessionManger = new SessionManger();
@Test
void sessionTest(){
// 세션 생성
MockHttpServletResponse response = new MockHttpServletResponse();
Member member = new Member();
sessionManger.createSession(member, response);
// 요청에 응답 쿠키 저장
MockHttpServletRequest request = new MockHttpServletRequest();
request.setCookies(response.getCookies());
// 세션 조회
Object result = sessionManger.getSession(request);
assertThat(result).isEqualTo(member);
// 세션 만료
sessionManger.expire(request);
Object expired = sessionManger.getSession(request);
assertThat(expired).isEqualTo(null);
}
}
10. 로그인 처리하기 - 서블릿 HTTP 세션1
1. HttpSession, JSessionId, tomcat 이론
- HttpSession 구조
- 세션 저장소 - Map 형태
- key : 어떤 종류의 데이터인지 나타내는 그냥 이름 - 상수로도 많이 작성
- value: 관련 데이터 복제본
- 세션 저장소의 저장소 - Map 형태
- key : 난수 - tomcat이 자동으로 만들어서 넣어줌
- value : 1. 의 세션 저장소 Map 통채로 값에 넣음 - setAttribute()해서 value에 map 형태의 값을 넣으면 된다.
- 1,2 둘다 HttpSession에 존재하는 것들이다.
- 세션 저장소 - Map 형태
- JSessionId=난수
- HttpRequest Header : cookie | JSessionId=6873@vsknqlFASDF....
-> 이전 직접만든 session의 SessionId=UUID 와 동일 - HttpSession이 존재할 때 Tomcat에서 스스로 sessionId=UUID를 JSession=톰켓이만든난수 이렇게 만들어줌
* 각 내장 서블릿에 따라서 sessionId이름이 달라진다. tomcat은 JSession이라고 한다. - HttpSession 저장소에 정보를 넣지 않더라도 HttpSesion객체가 존재만 하면 일단 만든다.
- HttpRequest Header : cookie | JSessionId=6873@vsknqlFASDF....
- 순서
- request가 온다.
- login에 성공할 시
- HttpRequest 존재 여부 확인 - default=true여서 존재 안할시 자동으로 만듬
- tomCat이 먼저 JSession=톰켓이만든난수 생성 == 세션 저장소의 저장소가 먼저 만들어짐
key로 난수를 가지고 value는 null 인 세션저장소의 저장소가 생성
- HttpRequest 존재 여부 확인 - default=true여서 존재 안할시 자동으로 만듬
- HttpSession.setAttribute(key, value) 세션 저장소 생성
- 3.번에서 만든 session저장소를 2.2번의 세션 저장소의 저장소에 value에 저장한다.
- request가 온다.
- 아래에 setting 할때 값을 뽑아서 쓸 때 사용하는 method와 연결해서 보여준다.
// 세션저장소의 저장소
{JSessionId==UUID, session==session 주소}
Map {session.getId(), request.getSession()} // 해당 data 추출때 사용하는 method
// 세션저장소
setAttribute("String", Object object)
session {getValueNames(),getAttribute()} // 해당 data 추출때 사용하는 method
- 그림으로 보여주는 HttpSession 과 JSessionId
2. HttpSession 코드 적용
- loginController
@PostMapping("/login")
public String loginV3(@Valid @ModelAttribute LoginForm form, BindingResult bindingResult, HttpServletRequest request) {
if (bindingResult.hasErrors()) {
return "login/loginForm";
}
Member loginMember = longinService.login(form.getLoginId(), form.getPassword());
if (loginMember == null){
bindingResult.reject("loginFail", "id or pw 맞지 않습니다");
return "login/loginForm";
}
// 로그인 성공 처리 TODO
// HttpSession이 존재하면 현 session을 반환, HttpSession이 존재 안할 시, HttpSession 객체 생성 (default true)
// Tomcat은 client의 request가 server로 넘어오고 HttpSesion이 생성될 때 "JSessionId = 난수" 가 자동으로 생성
// HttpSession 객체가 생성 시, 자동으로 TomCat이 {JSessionId = 난수} 를 생성한다.
HttpSession session = request.getSession();
// 세션에 로그인 회원 정보 보관
session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember); // setAttribute(getValueNames()==String, getAttribute()==Object)
log.info("session.getId()={}", session.getId()); // F7F6802308540042264B1F454581018D == JSessionId = UUID
log.info("request.getSession()={}", request.getSession()); // org.apache.catalina.session.StandardSessionFacade@78860b70
log.info("session.getValueNames()={}", session.getValueNames()); // loginMember
log.info("session.getAttribute()={}",session.getAttribute(SessionConst.LOGIN_MEMBER)); // Member(id=1, loginId=test, name=테스터, password=test!)
log.info("session.getAttributeNames()={}", session.getAttributeNames()); // 모든 session의 key값(session.setAttribute("key값", Object Value))을 enum으로 저장해서 보여준다.
return "redirect:/";
}
@PostMapping("/logout")
public String logoutV3(HttpServletRequest request){
HttpSession session = request.getSession(false);
if (session != null){
session.invalidate();
}
return "redirect:/";
}
HomeController
public String homeLoginV3(HttpServletRequest request, Model model) {
HttpSession session = request.getSession(false); // 세션저장소의 저장소에서 값만 가져옴 -> 난수(key)와 연결된 세션저장소(value) 가져옴
if (session == null) {
return "home";
}
// 세션 관리자에 저장된 정보 조회
// Member member = (Member)sessionManger.getSession(request);
Member loginMember = (Member) session.getAttribute(SessionConst.LOGIN_MEMBER);
// 세션저장소에서의 {key(SessionConst.LOGIN_MEMBER) value(저장한 data였던 Member 객체)} -> Member 객체 추출
// LOGIN_MEMBER는 앞전 SessionManger 상수와 동일 역할: 여러 session 중에서 해당 상수 이름을 가진 session을 찾고 session의 value(uuid)로 sessionStore의 value(=member)에 접근함
// 세션에 회원 데이터가 없으면 home
if (loginMember == null) {
return "home";
}
// 세션이 유지되면 로그인으로 이동
model.addAttribute("member", loginMember);
return "loginHome";
}
11. 로그인 처리하기 - 서블릿 HTTP Session2
11.1. @SessionAttrubte
- Spring이 세션을 편하게 사용할 수있도록 지원하는 어노테이션
- 해당 어노테이션은 세션이 없다고 세션을 생성하지는 않는다.
- 사용 예 - 이미 로그인 된 사용자를 찾을 때
@SessionAttribute(name = "loginMember", required = false) Member loginMember
// name에는 세션 저장소에 key로 넣었던 실제 값을 넣는다.
// Membert loginMember : key값을 통해 나오는 value의 type과 참조변수를 작성
- HomeController
- 이전에 작성한 homeController와 다동일하지만 @SessionAttribute를 통해서 /* ~~*/ 부분을 생략할 수 있게 되었다.
@GetMapping("/")
public String homeLoginV3Spring(@SessionAttribute(name = SessionConst.LOGIN_MEMBER, required = false) Member loginMember, 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";
}
- 첫 로그인 시 브라우저가 uri에 JSession=난수 를 표시한다.
- 이유 : 최초에 브라우저가 쿠키를 지원하는지 모르니깐 url을 통해서 세션을 유지하는 방법을 채택한 것이다.
- URL 전달 방식 끄기 설정하기
# application.properties
server.servlet.session.tracking-modes=cookie
2. 세션 정보와 타임아웃 설정
package hello.login.web.session;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.util.Date;
@Slf4j
@RestController
public class SessionInfoController {
@GetMapping("/session-info")
public String sessionInfo(HttpServletRequest request) {
HttpSession session = request.getSession(false);
if (session == null) {
return "세션이 없습니다.";
}
session.setMaxInactiveInterval(60);
// 세션 데이터 출력
session.getAttributeNames().asIterator()
.forEachRemaining(name -> log.info("session name = {}, value = {}", name, session.getAttribute(name)));
// JSessionId
log.info("sessionId={}", session.getId());
// 비활성화되는 session시간
log.info("maxInactiveInterval={}", session.getMaxInactiveInterval());
// 생성시간
log.info("CreationTime={}", new Date(session.getCreationTime()));
// 사용자가 마지막에 접근한 session 시간
log.info("LastAccessedTime={}", new Date(session.getLastAccessedTime()));
// 새 session
log.info("isNew={}", session.isNew());
return "세션출력";
}
}
- 중요한 기능
- maxInactiveInterval : 세션의 유효시간
- lastAccessedTime : 세션과 연결된 사용자가 최근에 서버에 접근한 시간
- 타임아웃 설정을 위한 정보를 알아본 이유
- 보통 사람들은 로그아웃을 직접 호출하지 않고 그냥 웹브라우저를 종료
- 서버 입장에서는 해당 사용자가 웹을 종료한것인지 잠시 자리를 뜬건지 모름
-> 세션 데이터를 언제 삭제해야 하는지 판단이 어렵다. - 그냥 세션을 남겨뒸을 때 문제점
- 쿠키 해킹당하면 안좋게 사용당할 가능성이 높아진다.
- 메모리에 세션이 생성되는데 메모리 크기가 무한하지 않으므로 용량조절이 필요
- 해결방안
- 세션 생성 시점으로부터 30분으로 잡는다던지 한다.
- 세션과 연결된 사용자와 서버의 최근 연결시간이 30분을 넘으면 삭제 되도록
-> 사용자가 쓰고 있으면 최근 연결시간이 계속 갱신되서 세션 유지가 된다.
- 세션 생성 시점으로부터 30분으로 잡는다던지 한다.
- 설정방법
- HttpSession 자체가 위의 해결방안 방식을 채택하고 있어서 그냥 최근 접촉한 session시간에서 몇분 지나면 삭제할지 시간만 설정해주면 된다.
- 글로벌 설정 - 분 단위로 설정
- 특정 세션 단위 시간 설정
# 글로벌 설정
# application.properties
server.servlet.session.timeout=60 // 60초 - default 30분
# 특정 세션 단위로 시간 설정
session.setMaxInactiveInterval(1800) // 1800초
- 정리
- 세션에는 최소한의 데이터만 보관해야 한다.
- 보관한 데이터 용량 * 사용자 수 = 메모리 사용량
- 급격하게 사용자 수가 늘시 장애로 이어질 수 있다.
- default 시간이 30분인 것을 알고 이를 기준으로 적절한 시간을 고민할 것