Home Session
Post
Cancel

Session

세션

  • 서버는 로그인을 통해 요청을 보낸 사용자를 구분한다. 하지만 모든 요청마다 ID/PW를 확인할 수 없으므로 토큰을 발급하고 세션에는 토큰을 저장해 해당 세션이 유지되는 동안 로그인없이 토큰만 가지고 사용자를 인증한다.

SecurityContextPersistenceFilter

  • 세션이 유지되고 있는 동안 SecurityContext가 유지되도록 도와준다.
  • 세션에 SecurityContext를 보관했다가 다음 Request에서 넣어준다.
  • SecurityContextPersistenceFilter는 기본적으로 SecurityContext를 저장하는 저장소를 가지고 있다. Default로 Http Session의 security context를 저장하는 Repository를 사용하도록 되어 있다(HttpSessionSecurityContextRepository).
    • 로그인 후 Request는 Session에서 SecurityContext를 가져와 SecurityContextHolder에 넣어준다.
    • 이후 Filter는 Holder에 Authentication이 들어있기 때문에 로그인한것으로 간주한다.
  • Http Session안에는 CSRF Token값과 Spring Security Context를 가지고있다.
  • ThreadLocal이 처리를 끝내고 나갈 때에는 SecurityContext를 Clear해준다.

Session ID 확인하기

  • Chrome의 경우 개발자 도구 -> 애플리케이션 -> 쿠키 에서 확인 가능

  • SessionID Console에 출력

    1
    2
    3
    4
    5
    
      @GetMapping("/")
      public String main(Model model, HttpSession session){
          System.out.println(session.getId());
          return "index";
      }
    
  • Session timeout 설정

    1
    2
    3
    4
    5
    
      server:
        port: 9056
        servlet:
          session:
            timeout: 60s
    

RememberMeAuthenticationFilter

  • 인증 정보를 세션으로 관리하는 경우, 세션이 만료된 후 remember-me 쿠키를 사용해 로그인을 기억했다가 자동으로 재로그인 시켜주는 기능이다.
  • RememberMeAuthenticationFilter는 RememberMeService를 가지고있다.
  • RememberMeService를 구현한 AbstractRememberMeService가 있고 AbstractRemberMeService를 확장해 구현한 구현체로 TokenBasedRememberMeService와 PersistenceTokenBasedRememberMeService가 있다. (설정을 따로 하지 않는 경우 TokenBasedRememberMeService가 사용된다.)
  • Remeberme로 로그인한 사용자는 UsernamePasswordAuthenticationToken 이 아닌 RememberMeAuthenticationToken 으로 서비스를 이용하는 것이다. 같은 사용자이긴 하지만, 토큰의 종류가 다르게 구분되어 있다.
  • Session이 유지되고 있는 동안에는 Session Filter를 타고 Remember-Me Filter는 통하지 않는다.

TokenBasedRememberMeService

  • 토큰을 브라우저에 저장한다.
  • 쿠키로 전송되는 포멧: 아디이:만료시간:Md5Hex(아이디:만료시간:비밀번호:인증키)
    • 서명값을 통해 서버에서 인증 여부를 판단한다.
  • 만약 User가 password를 바꾼다면 토큰을 사용할 수 없고 만료 기간이 지나지 않은 토큰이 탈취된다면 비밀번호를 변경하지 않는 한 해결할 방법이 없다(기본 유효시간은 14일).

PersistenceTokenBasedRememberMeService

  • 토큰을 서버에 저장한다.
  • 쿠키로 전송되는 포멧: series:token
    • 토큰에 username 및 만료시간이 노출되지 않는다.
    • PersistentTokenRepository에 저장하며 usernmae, series, token, last_used정보를 저장한다.
  • 재로그인 될 때마다 series값이 갱신되며
  • series값이 키가 된다. 일종의 채널이라고 보면 편리하다.
  • 재로그인 될 때마다 series값을 갱신해준다. 따라서 토큰이 탈취되어 다른 사용자가 다른 장소에서 로그인을 했다면 정상 사용자가 다시 로그인 할 때, CookieTheftException이 발생하게 되고, 서버는 해당 사용자로 발급된 모든 remember-me 쿠키값들을 삭제하고 재로그인을 요청하게 된다.
  • InmemoryTokenRepository 는 서버가 재시작하면 등록된 토큰들이 사라진다. 따라서 자동로그인을 설정했더라도 다시 로그인을 해야 한다. 재시작 후에도 토큰을 남기고 싶다면 JdbcTokenRepository를 사용하거나 이와 유사한 방법으로 토큰을 관리해야 한다.
  • 로그아웃하면 다른 곳에 묻혀놓은 remember-me 쿠키값도 쓸모가 없게 된다. 만약 다른 곳에서 remember-me로 로그인한 쿠키를 살려놓고 싶다면, series 로 삭제하도록 logout 을 수정해야 한다.

RememberMe Filter 사용하기

  • TokenBasedRememberMeService 사용 (Default)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    
      @Override
      protected void configure(HttpSecurity http) throws Exception {
          http
              .authorizeRequests(request->
                  request.antMatchers("/").permitAll()
                  .anyRequest().authenticated()
              )
              .formLogin(login->
                  login.loginPage("/login")
                  .loginProcessingUrl("/loginprocess")
                  .permitAll()
                  .defaultSuccessUrl("/", false)
                  .failureUrl("/login-error")
              )
              .logout(logout->
                  logout.logoutSuccessUrl("/"))
                  .exceptionHandling(error->
                  error.accessDeniedPage("/access-denied")
              )
              .rememberMe(
                  // r->r.alwaysRemember(true)
                  );  // 추가
      }
    
    • rememberMe를 넣는것으로 filter를 추가할 수 있다.
    • 첫 로그인시 Loginform에 name이 remember-me인 값이 true로 서버로 전송하고 UserNamePasswordAuthenticationFilter에서 인증이 성공하면 RememberMeAuthenticationService에게 인증 성공 노티가 가게된다. (쿠키에 remember-me 쿠키를 저장해 전송)
      • 만약 요청에 상관없이 항상 rememberMe를 설정하고 싶다면 alwaysRemember를 true로 설정하면된다.
    • 로그인 성공한 뒤 메인 페이지에서 Session이 만료되어도 정상적으로 유저 페이지에 접속이 가능하다.
    • Remember-Me 탈취
      • Remember Me Service를 설정하지 않았으므로 TokenBasedRememberMeService가 사용되고 브라우저에 저장된 Remember-Me 토큰을 복사해 다른 브라우저에서 사용하면 이전 브라우저에서 로그인한 User로 접속을 할 수 있게된다.
      • 비밀번호를 변경하면 요청은 이전 비밀번호로 생성된 서명값이 들어있고 서버에서는 변경된 비밀번호 값으로 서명을 만들어 검증을 하기 때문에 remember-me 쿠키가 유효하지 않게된다.
  • PersistenceTokenBasedRememberMeService사용

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    
      @EnableWebSecurity
      @EnableGlobalMethodSecurity(prePostEnabled = true)
      public class SecurityConfig extends WebSecurityConfigurerAdapter {
        
          SecurityContextPersistenceFilter filter;
          private final MemberService memberService;
          private final DataSource dataSource;
        
          public SecurityConfig(MemberService memberService, DataSource dataSource) {
              this.memberService = memberService;
              this.dataSource = dataSource;
          }
        
      		....
        
          @Bean
          PersistentTokenRepository tokenRepository(){
              JdbcTokenRepositoryImpl repository = new JdbcTokenRepositoryImpl(); 
              // JdbcTokenRepositoryImpl에 스키마와 create query등 모든게 다 작성되어 있다.
              repository.setDataSource(dataSource); // dataSource 주입 필요
        
              //JdbcTokenRepositoryImpl은 최초에 한번만 Table생성을 해야하는데 (IF없이 무조건 생성하도록 Create가 구현되어있어서)
              //아래와같은 편법을 사용해 가능하다. 없을 것 같은 UserToken을 지우기를 시도하고 예외가 발생하면 Table이 없기 때문일 것이므로 Table을 생성한다.
              //Table이 있는데 1이 없다면 Remove의 구현상 아무런 동작 없이 넘어갈 것이다.
              try{
                  repository.removeUserTokens("1");
              }catch (Exception e){
                  repository.setCreateTableOnStartup(true);
              }
              return repository;
          }
        
          @Bean
          PersistentTokenBasedRememberMeServices rememberMeServices(){
              //1. DB에 저장을 하면 이전 저장값이 유효해야 하기 때문에 반드시 Key값이 필요하다.
              //2. UserDetailsService
              //3. PersistentTokenRepository 
              PersistentTokenBasedRememberMeServices service =
                      new PersistentTokenBasedRememberMeServices("anything",
                              memberService,
                              tokenRepository());
              // service.setAlwaysRemember(true);    alwaysRemeber를 설정했다면 
              return service;
          }
        
          @Override
          protected void configure(HttpSecurity http) throws Exception {
              http
                      .authorizeRequests(request->
                          request.antMatchers("/").permitAll()
                                  .anyRequest().authenticated()
                      )
                      .formLogin(login->
                              login.loginPage("/login")
                              .loginProcessingUrl("/loginprocess")
                              .permitAll()
                              .defaultSuccessUrl("/", false)
                              .failureUrl("/login-error")
                      )
                      .logout(logout->
                              logout.logoutSuccessUrl("/"))
                      .exceptionHandling(error->
                              error.accessDeniedPage("/access-denied")
                      ).rememberMe(r->r.rememberMeServices(rememberMeServices())); //rememberMe Service를 직접 만든것으로 사용하도록
          }
        
        ....
      }
    
    • 이제 토큰을 DB에 저장하게 된다.
    • 로그인을 해보면(remember-me) DB와 Browser에 모두 remember-me 토큰이 있는것을 확인 할 수 있다.

      browser

      db

      • Browser에 저장된 쿠키를 Decoding해 보면 QehOjU0A43lJ0yTmA4Vzgw%3D%3D:MN6JiYch24DPUcD6sNhHuA%3D%3D와 같이 나오는데 이전과 같이 아이디:만료시간:비밀번호:인증키 형식이 아닌 series:token 형식인 것을 알 수 있다.
      • 세션만료 후 remember-me filter가 동작하게 하면 유효시간 검증, series와 token값을 DB값을 비교 한 뒤 새로운 토큰을 발행한다. (토큰을 계속 갱신)
      • 토큰을 갱신하는 이유
        • 악의 적인 사용자가 Token을 탈취해 로그인한 경우 DB의 Token값이 갱신된다.
        • 악의 적인 사용자는 변경된 Token값을 가지고 있지만 기존 사용자는 갱신 된 Token값을 모른다. 기존 사용자가 세션 만료 후 remember me filter를 거치면 DB와 Browser의 Token 불일치가 발생한다. 서버는 이러한 Token불일치가 발생하면 해당 User에게 발행되었던 모든 remember-me Token을 삭제한다.

세션 관리

  • Spring이 직접 Session을 제어할 수 없다. Tomcat과 같은 ServletContainer에서 제공하며 Tomcat이 넘겨주는 Session을 SessionInformation 객체로 SessionRegistry에서 관리한다.
  • SessionInformation은 sessionId, principal, expired, lastRequest 정보를 가지고 있다.

ConcurrentSessionFilter

  • 동시 접속에만 관심을 가진다.
    • SessionRegistry에 SessionInformation에서 expired된 Token이 들어오지 못하도록 관리한다.
    • 동시접속 제어 효과를 얻을 수 있다.
  • 만료된 세션에 대한 요청인 경우 세션 즉시 종료시킨다.
  • 세션 만료에 대한 판단은 SessionManagementFilter 의 ConcurrentSessionControlAuthenticationStrategy 에서 처리한다.
  • SessionRegistry의 세션 정보를 expire시키면 톰켓에서 세션이 허용되어도 애플리케이션으로 들어올 수 없다.
  • SessionRegistry에는 Authentication으로 등록된 사용자만 모니터링한다.

SessionManagementFilter

  • 어떤 경우에 어떤 Session에 대해서 expire를 시킬지 결정 한다. (Session 관리)
  • SessionManagementFilter는 SessionAuthenticationStrategy를 가지고 있다.
  • SessionAuthenticationStrategy는 Authentication이 발생했을 때 Session에 어떤 전략을 적용할지 정한다.
    • Session 고정 정책
    • 동시 접속 제어 정책

Session Id가 고정될 경우 문제

  • sessionId는 로그인할 때마다 Id가 바뀌게 되어있다. → 만약 SessionId가 고정된다면 아래와 같은 시나리오의 문제점이 생길 수 있다.
    1. 정상 사용자의 브라우저에 공격자 브라우저의 세션값을 주입한다.
    2. 정상 사용자가 해당 사이트에 로그인 하기만 하면 공격자 브라우저로 마치 인증된 사용자인 것처럼 리소스에 접근할 수 있다.
      • 두 브라우저의 Session이 같기 때문

SessionManagementFilter추가

1
2
3
4
5
6
.sessionManagement(
    s->s.
    .maximumSessions(1)       //max Session개수
    .maxSessionsPreventsLogin(false)  //새로 들어온 Session을 인정하고 기존 Session을 expire
    .expiredUrl("/session-expired")    //expired session이 갈 URL
);
  • 동시 접속을 할 경우 최대 세션개수 1, maxSessionsPreventsLogin를 false로 설정했기 때문에 나중에 로그인한 user의 session만 유효하다.

Session Creation Policy

1
2
3
4
5
6
7
.sessionManagement(
    s->s.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) // session Creation Policy
    .sessionFixation(sessionFixationConfigurer -> sessionFixationConfigurer.none()) // none으로 설정시 session을 바꾸지 않는다. -> default는 changeSessionId
    .maximumSessions(1)       //max Session개수
    .maxSessionsPreventsLogin(false)  //새로 들어온 Session을 인정하고 기존 Session을 expire
    .expiredUrl("/session-expired")    //expired session이 갈 URL
);
  • Always : 항상 세션을 생성함
  • If_Required : 필요시 생성
  • Never : 생성하지 않음.
  • Stateless : 생성하지 않음. 존재해도 사용하지 않음.
This post is licensed under CC BY 4.0 by the author.