세션
- 서버는 로그인을 통해 요청을 보낸 사용자를 구분한다. 하지만 모든 요청마다 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에 저장된 쿠키를 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가 고정된다면 아래와 같은 시나리오의 문제점이 생길 수 있다.
- 정상 사용자의 브라우저에 공격자 브라우저의 세션값을 주입한다.
- 정상 사용자가 해당 사이트에 로그인 하기만 하면 공격자 브라우저로 마치 인증된 사용자인 것처럼 리소스에 접근할 수 있다.
- 두 브라우저의 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 : 생성하지 않음. 존재해도 사용하지 않음.