framework/spring

스프링 mvc2 - 4. 로그인 처리1 - 쿠키, 세션

wooweee 2023. 5. 6. 15:36
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 요청시마다 쿠키를 요청에 넣어서 서버에 전달
  • 쿠키 종류
    1. 영속쿠키: 만료 날짜를 입력시 해당 날짜까지 유지
    2. 세션쿠키: 만료 날짜 생략시 브라우저 종료시 까지만 유지

 

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가지 상황을 고려해서 수정 할 것이다.
      1. 홈 controller는 로그인하지 않은 사람들도 봐야한다. -> home.html 화면을 보여준다.
      2. cookie 중에서도 오래되서 쿠키는 존재하는데 실제 repository에 값이 없는 경우 -> home.html 화면을 보여준다.
      3. 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); // 새로 쓴 쿠키 보내주기
}

 

개념

  • 사용자가 보낸 쿠키를 사용하지 않고 새로 쿠키를 생성하는 이유, 기존의 쿠키는 어떻게 되는가?(삭제되는지, 수정되는지)
    1. 사용자의 브라우저에 저장된 쿠키를 만료시켜야 하기 때문 
      사용자가 서버로 보낸 쿠키는 사용자의 브라우저에 저장된 쿠키의 복사본이다.
      따라서 사용자의 브라우저에 저장된 쿠키(member id)를 만료시키려면 서버에서 동일한 이름(member id)으로 쿠키를 만들고

    2. 브라우저에 저장된 쿠키를 새로운 쿠키로 덮어쓰게 만들어야 한다.

  • 서버에서 새롭게 쿠키를 만들어 사용자에게 전달하는 방법 외에 사용자의 브라우저에 저장된 쿠키를 제어하는 방법은 없다.
  • logout도 응답 쿠키를 생성 : Max-Age=0을 확인할 수 있다.

 

6. 쿠키와 보안 문제

 

  • 쿠키를 사용한 로그인 Id의 보안 문제들
    1. 쿠키 값은 임의로 변경이 가능 -> 나의 쿠키를 보고 다른 사람의 쿠키도 예측이 가능해진다.
    2. 쿠키에 보관된 정보는 훔쳐갈 수 있다. -> 쿠키 정보가 나의 로컬 PC에서 털릴 수 있고 네트워크 전송 구간에서 털릴 수 있다.
    3. 해커가 쿠키를 한번 훔치면 평생 사용이 가능하다.
    4. 쿠키는 쿠키 정보를 이용해서 db의 실제 값을 이용해서 응답에 사용하는데, db를 직접 건들이기 때문에 쿠키가 털렸다고 db 정보를 함부로 건들일 수 없다.

  • 대안
    • 쿠키에 중요한 값을 노출하지 않고 예측불가능한 토큰(랜덤 값)을 노출하고 서버에서 토큰과 사용자 id를 매핑해서 인식
    • 토큰은 서버에서 관리하도록
    • 토큰은 해커가 임의의 값을 넣어도 찾을 수 없도록 예상 불가해야한다.
    • 토큰을 해커가 털어도 만료시간을 짧게 유지한다.
  • 대안 결론
    • 세션을 이용

 

7. 로그인 처리하기 - 세션 동작 방식

  • 쿠키
    1. 노란색 상자속 쿠키를 보게되면 1번이 memberId라는 것이 너무 가독성이 좋다
    2. 1 이란 쿠키 값이 어디서 어떻게 사용되는 것인지 확실히 알 수 있다.
    3. memberId로 표시 안할려고 하니 코드 내부적으로 가독성이 너무 떨어지는 안좋은 방향으로 흘러가게 된다.
    4. 쿠키와 실제 회원저장소와 직접적으로 연결이 되어있어서 해킹되었을 때 실제 db에 영향을 주게 된다.

 

  • 세션
    1. 세션을 털어도 가독성이 떨어진다. 그저 mySessionId로만 되어있어서 어떤 위치의 data인지 알 수가 없다.
    2. 또한 어떤 data인지 알더라도 다른 사람들의 sessionId를 예상 못한다.
    3. session 시간을 정했기 때문에 sessionId 삭제되면 해킹에 진도에 차질이 생긴다.
    4. 첫 로그인 이후로는 실제 db가 아닌 임시로 생성한 세션저장소(복사본이 저장되어있다.)에서부터 db에서 받아와야하는 값을 받아오기 때문에 해킹이되더라도 실제 db는 보호받을 수 있다. 세션 저장소의 entity만 얼른 제거하던가 세션을 만료시키면 조금이나마 방어가 가능하다.

 

  • 세션 작동 방식
    • 쿠키와 전반적인 작동원리는 전반적으로 동일 - 차이점 : 세션 저장소, 세션 아이디(UUID)가 존재한다는 차이점

    • 먼저 세션 저장소(table 형태)를 생성

    • 로그인이 성공한 후 서버에서 쿠키를 넘겨줄 때 
      1. 먼저 data를 db에 저장한다.

      2. 세션 저장소의 {key:value} 에 저장 - db repository를 사용하지 않고 세션 저장소를 이용한다.
        • key : 난수로 되어있는 sessionId
        • value: db에서 가지고 온 data
      3. session 저장소의 key값을 cookie에 담아서 보낸다.
        cookie또한 key-value로 구성되어있다.
        • cookie key    :  "mySessionId" (그냥 별의미 없는 이름)
        • cookie value : sessionId(난수)

    • 참고
      • Set-Cookie:는 지정한 이름이 아니라 request header의 Accept, Cache, 등등 목록의 이름이라고 생각하면된다.
      • -> 브라우저에서 봤을 때 Cookie라고 되어있었다. Set-Cookie = Cookie란 동일하다.

 

  • 정리
    1. 쿠키 값 변조 -> 얘상 불가한 복잡한 sessionId 사용으로 예측 불가 시킴

    2. 쿠키 보관하는 정보는 클라이언트 해킹시 털릴 가능성이 존재 -> 세션Id가 털려도 중요 정보를 가지고 있지 않다. 또한 이 data 자체로는 어떤 data인지 추정이 불가하다.

    3. 쿠키 탈취 후 사용 -> 시간이 지나면 사용할 수 없도록 서버에서 세션의 만료시간을 짧게 유지 하거나 해당 세션을 강제로 제거 -> data를 막 제거할 수 있는 이유는 실제 db면 데이터 제거가 힘들지만 session저장소의 data는 복제된 값이므로 막 삭제해도 상관 없다.

 

8. 로그인 처리하기 - 세션 직접 만들기

  • 3가지의 기능 제공
    1. 세션 생성
      • sessionId 생성 (임의의 추정 불가능한 랜덤 값)
      • 세션 저장소에 sessionId와 보관할 값 저장
      • sessionId로 응답 쿠키를 생성해서 클라이언트에 전달
    2. 세션 조회
      • 클라이언트가 요청한 session 쿠키의 값으로, 세션 저장소에 보관한 값 조회
    3. 세션 만료
      • 클라이언트가 요청한 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 구조

    1. 세션 저장소 - Map 형태
      • key : 어떤 종류의 데이터인지 나타내는 그냥 이름 - 상수로도 많이 작성
      • value: 관련 데이터 복제본

    2. 세션 저장소의 저장소 - Map 형태
      • key : 난수 - tomcat이 자동으로 만들어서 넣어줌
      • value : 1. 의 세션 저장소 Map 통채로 값에 넣음 - setAttribute()해서 value에 map 형태의 값을 넣으면 된다.

    3. 1,2 둘다 HttpSession에 존재하는 것들이다.

  • JSessionId=난수
    • HttpRequest Header :  cookie |  JSessionId=6873@vsknqlFASDF.... 
      -> 이전 직접만든 session의 SessionId=UUID 와 동일

    • HttpSession이 존재할 때 Tomcat에서 스스로 sessionId=UUID를 JSession=톰켓이만든난수 이렇게 만들어줌
      * 각 내장 서블릿에 따라서 sessionId이름이 달라진다. tomcat은 JSession이라고 한다.

    • HttpSession 저장소에 정보를 넣지 않더라도 HttpSesion객체가 존재만 하면 일단 만든다.

  • 순서
    1. request가 온다.

    2. login에 성공할 시
      1. HttpRequest 존재 여부 확인 - default=true여서 존재 안할시 자동으로 만듬

      2. tomCat이 먼저 JSession=톰켓이만든난수 생성 == 세션 저장소의 저장소가 먼저 만들어짐
        key로 난수를 가지고 value는 null 인 세션저장소의 저장소가 생성
    3. HttpSession.setAttribute(key, value) 세션 저장소 생성

    4. 3.번에서 만든 session저장소를 2.2번의 세션 저장소의 저장소에 value에 저장한다. 

  • 아래에 setting 할때 값을 뽑아서 쓸 때 사용하는 method와 연결해서 보여준다.
// 세션저장소의 저장소
{JSessionId==UUID, session==session 주소}
Map {session.getId(), request.getSession()} // 해당 data 추출때 사용하는 method

// 세션저장소
setAttribute("String", Object object) 
session {getValueNames(),getAttribute()} // 해당 data 추출때 사용하는 method

 

  • 그림으로 보여주는 HttpSession 과 JSessionId

녹색 박스 안의 모든 테이블이 HttpSession을 통해 생성된 것이다.

 

 

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 "세션출력";
    }
}

 

  • 중요한 기능
    1. maxInactiveInterval : 세션의 유효시간
    2. lastAccessedTime : 세션과 연결된 사용자가 최근에 서버에 접근한 시간

  • 타임아웃 설정을 위한 정보를 알아본 이유
    • 보통 사람들은 로그아웃을 직접 호출하지 않고 그냥 웹브라우저를 종료
    • 서버 입장에서는 해당 사용자가 웹을 종료한것인지 잠시 자리를 뜬건지 모름
      -> 세션 데이터를 언제 삭제해야 하는지 판단이 어렵다.
    • 그냥 세션을 남겨뒸을 때 문제점
      • 쿠키 해킹당하면 안좋게 사용당할 가능성이 높아진다.
      • 메모리에 세션이 생성되는데 메모리 크기가 무한하지 않으므로 용량조절이 필요
  • 해결방안
    • 세션 생성 시점으로부터 30분으로 잡는다던지 한다.

    • 세션과 연결된 사용자와 서버의 최근 연결시간이 30분을 넘으면 삭제 되도록
      -> 사용자가 쓰고 있으면 최근 연결시간이 계속 갱신되서 세션 유지가 된다.

  • 설정방법
    • HttpSession 자체가 위의 해결방안 방식을 채택하고 있어서 그냥 최근 접촉한 session시간에서 몇분 지나면 삭제할지 시간만 설정해주면 된다.
    • 글로벌 설정 - 분 단위로 설정
    • 특정 세션 단위 시간 설정
# 글로벌 설정
# application.properties
server.servlet.session.timeout=60 // 60초 - default 30분

# 특정 세션 단위로 시간 설정
session.setMaxInactiveInterval(1800) // 1800초

 

 

  • 정리
    • 세션에는 최소한의 데이터만 보관해야 한다.
    • 보관한 데이터 용량 * 사용자 수 = 메모리 사용량
    • 급격하게 사용자 수가 늘시 장애로 이어질 수 있다.
    • default 시간이 30분인 것을 알고 이를 기준으로 적절한 시간을 고민할 것