스프링부트 세션 저장 - seupeulingbuteu sesyeon jeojang

Spring으로 개발하면 주로 화면을 JSP나 Thymeleaf를 쓰는곳이 많이 있다. 이 두가지를 쓰면서 제일 많이하는 것중 하나가 session 또는 model에 데이터를 담아서 view 페이지와 함께 렌더링 하는 작업을 많이하게 되는데
이때 주의해야할 점이 있다. 예제코드를 살펴보자.

스프링부트 세션 저장 - seupeulingbuteu sesyeon jeojang

회원 아이디로 DB에서 회원정보를 조회한 후 'member' 라는 key로 session에 저장한 코드이다.

스프링부트 세션 저장 - seupeulingbuteu sesyeon jeojang

session에 저장된 회원의 이름과 주소를 출력해보았지만 저장된 정보가 나오지 않았다.

왜 나오지 않는지 스프링 코드를 한번 살펴보자

RequestAttributes.java

스프링부트 세션 저장 - seupeulingbuteu sesyeon jeojang

Spring에서는 RequestAttributes 인터페이스를 제공하는데 request와 session의 scope 우선순위값을 관리하는데
request의 scope상태가 더 높다. 여기서 더 높다라는 의미는

JSTL이나 Thymeleaf 템플릿 문법으로 데이터를 화면 에 그릴때 request scope에 존재하는 데이터를 찾고 없으면 session scope에 존재한 데이터를 찾는다.

스프링부트 세션 저장 - seupeulingbuteu sesyeon jeojang

이 코드를 자세히 살펴보면 이상한점이 있다.
'member' 라는 key로 session에 저장해두었고 화면에서 출력할땐 request scope엔 존재하지 않지만
session scope엔 존재하니 데이터가 나와야 되는게 정상아닌가?

사실 'member' 라는 key는 이미 페이지가 렌더링이 되기전 request scope에 저장되있었다.

ModelAttributeMethodProcessor

스프링부트 세션 저장 - seupeulingbuteu sesyeon jeojang

요청이 들어오면 ModelAttributeMethodProcessor가 동작하게 된다. 번역기를 돌려보면

메서드 매개변수에 @ModelAttribute 어노테이션이 선언되있다면 해당 기본생성자를 호출하여 인스턴스를 생성하고
요청 매개변수에 데이터를 바인딩을 해주며 모델에 추가된다.

@ModelAttribute 어노테이션은 생략이 가능하다.

스프링부트 세션 저장 - seupeulingbuteu sesyeon jeojang

Model은 key, value구조다보니 내부적으로 Map의 entry형태로 이뤄진걸 알 수 있어서 entry 값들을 확인해본 결과
이미 'member' 라는 key가 model에 저장되며 model에 저장하면 내부적 request에 저장이 된다.'

@ModelAttribute 어노테이션이 선언후 인자에 선언된 key값으로 model에 담기며 어노테이션이 생략시

파라미터 타입 클래스이름의 맨 앞글자만 소문자로 바꾼 값이 key가 된다.

마무리

스프링부트 세션 저장 - seupeulingbuteu sesyeon jeojang

session에 저장된 데이터를 가져올땐 'session' 이라는 값을 앞에다 붙혀주면 된다.

스프링/Spring MVC

[SpringBoot] 세션 직접 만들어서 로그인 처리하기

여행가는 개발자 2021. 10. 25. 22:21

스프링부트 세션 저장 - seupeulingbuteu sesyeon jeojang

스프링 부트에서 로그인을 처리하기 위해서는 서블릿이 지원하는 HttpSession을 사용하면 쉽게 구현이 가능합니다.

하지만 세션에 대한 이해도를 높이기 위해 직접 만들어보기로 하겠습니다. 이전에 웹브라우저와 서버 간에 로그인 상태를 유지하는 방법에 대해서 알아보겠습니다.

 

 

로그인 상태 유지하기

로그인 상태를 유지하기 위해 대표적으로 다음과 같은 방법이 있습니다.

1.  요청할 때 사용자 정보를 쿼리 파라미터로 전달하기

사용자 정보를 쿼리 파라미터를 계속 유지하면서 보내는 것은 매우 어렵고 번거로운 작업입니다. 게다가 식별 가능한 사용자 정보를 쿼리 파라미터에 노출하는 것은 보안상 위험합니다.

2. 쿠키에 사용자 정보 담아서 사용하기

서버에서 로그인에 성공하면 HTTP 응답에 쿠키를 담아서 브라우저에 전달합니다. 그러면 브라우저는 앞으로 해당 쿠키를 지속해서 서버로 보내줘서 서버는 사용자를 식별할 수 있습니다.

Cookie: memberId=1

물론 여기에도 보안 문제가 있습니다.

  • 쿠키 값은 임의로 변경할 수 있음.
    • 클라이언트가 쿠키를 강제로 변경하면 다른 사용자가 됨.
    • 실제 웹브라우저 개발자 모드 Application Cookie 변경으로 확인 가능
    • Cookie: memberId=1 Cookie: memberId=2 (다른 사용자의 이름이 보임)
  • 쿠키에 보관된 정보는 훔쳐갈 수 있음.
    • 만약 쿠키에 개인정보나, 신용카드 정보가 있다면?
    • 이 정보가 웹 브라우저에도 보관되고, 네트워크 요청마다 계속 클라이언트에서 서버로 전달됨
    • 쿠키의 정보가 나의 로컬 PC가 털릴 수도 있고, 네트워크 전송 구간에서 털릴 수도 있음.
  • 해커가 쿠키를 한번 훔쳐가면 평생 사용할 수 있음.
    • 해커가 쿠키를 훔쳐가서 그 쿠키로 악의적인 요청을 계속 시도할 수 있음

 

쿠키에는 영속 쿠키와 세션 쿠키가 있습니다.
  • 영속 쿠키: 만료 날짜를 입력하면 해당 날짜까지 유지
  • 세션 쿠키: 만료 날짜를 생략하면 브라우저 종료 시까지만 유지

브라우저 종료시 로그아웃이 되길 원하면 세션 쿠키를 사용하면 됩니다.

 

3. 쿠키에 세션 정보를 담아서 사용하기

세션 생성

세션 ID를 생성하는데, 추정 불가능해야 한다. UUID는 추정이 불가능합니다.

Cookie: mySessionId=zz0101xx-bab9-4b92-9b32-dadb280f4b61

생성된 세션 ID와 세션에 보관할 값( memberA )을 서버의 세션 저장소에 보관합니다.

클라이언트와 서버는 결국 쿠키로 연결이 되어야 합니다.

서버는 클라이언트에 mySessionId라는 이름으로 세션 ID 만 쿠키에 담아서 전달합니다

클라이언트는 쿠키 저장소에 mySessionId 쿠키를 보관합니다.

 

중요
여기서 중요한 포인트는 회원과 관련된 정보는 전혀 클라이언트에 전달하지 않는다는 것이다.
오직 추정 불가능한 세션 ID만 쿠키를 통해 클라이언트에 전달한다

 

 

세션 관리하기 

크게 다음 3가지 기능을 제공하면 됩니다.

세션 생성

sessionId 생성 (임의의 추정 불가능한 랜덤 값)

세션 저장소에 sessionId와 보관할 값 저장

sessionId로 응답 쿠키를 생성해서 클라이언트에 전달

세션 조회

클라이언트가 요청한 sessionId 쿠키의 값으로, 세션 저장소에 보관한 값 조회

세션 만료

클라이언트가 요청한 sessionId 쿠키의 값으로, 세션 저장소에 보관한 sessionId와 값 제거

 

 

SessionManager - 세션 관리

/**
 * 세션 관리
 */
@Component
public class SessionManager {

    private 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());
        }
    }

    private Cookie findCookie(HttpServletRequest request, String cookieName) {
        return Arrays.stream(request.getCookies())
                .filter(cookie -> cookie.getName().equals(cookieName))
                .findAny()
                .orElse(null);
    }


}
  • ConcurrentHashMap : HashMap 은 동시 요청에 안전하지 않다. 동시 요청에 안전한 ConcurrentHashMap를 사용했다.

 

SessionManagerTest - 테스트

class SessionManagerTest {

    SessionManager sessionManager = new SessionManager();

    @Test
    void sessionTest(){

        // 세션 생성 및 Http 응답을 받고 세션을 쿠키에 담고, response에 쿠키가 담김
        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();
    }

}

여기서는 HttpServletRequest , HttpservletResponse 객체를 직접 사용할 수 없기 때문에, 테스트에서 비슷한 역할을 해주는 가짜 MockHttpServletRequest, MockHttpServletResponse를 사용했습니다.

 

LoginController - 로그인 (세션 생성)

@Controller
@Slf4j
@RequiredArgsConstructor
public class LoginController {
    private final LoginService loginService;
    private final SessionManager sessionManager;
    
    @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("loginFali", "아이디 또는 비번이 맞지 않아~~");
            return "login/loginForm";
        }

        // 로그인 성공 처리
        // 세션 관리자를 통해 세션을 생성하고, 회원 데이터를 보관
        sessionManager.createSession(loginMember, response);
        return "redirect:/";
	}
}
  • private final SessionManager sessionManager; 주입
  • sessionManager.createSession(loginMember, response); 로그인 성공 시 세션을 등록한다. 세션에 loginMember를 저장해 두고, 쿠키도 함께 발행합니다.

 

HomeController - 홈 화면 요청 (세션 조회)

세션 보유 여부에 따라 다른 페이지 이동시키도록 했습니다.

@GetMapping("/")
public String homeLogin(HttpServletRequest request, Model model) {

        Member member = (Member) sessionManager.getSession(request);
        if (member == null) {

            return "home";
        }

        model.addAttribute("member", member);
        return "loginHome";
}

 

LoginController - 로그아웃 (세션 만료시키기)

쿠키의 만료 시간인 MaxAge를 0으로 설정함으로써 쿠키를 만료시킵니다.

@PostMapping("/logout")
public String logoutV(HttpServletRequest request) {
  sessionManager.expire(request);
  return "redirect:/";
}

 

Spring에서 지금까지 로그인 상태 유지하는 방법과 세션을 직접 개발하는 방법을 알아봤습니다.

다음에는 서블릿이 지원하는 HttpSession을 사용해 세션을 개발해보겠습니다. 

저작자표시