본문 바로가기

개인적으로 공부한 것을 정리해 놓은 블로그입니다 틀린 것이 있으면 댓글 부탁 드립니다!


Spring Security

스프링시큐리티 공부 12 - 세션관리 (ConcurrentSessionFilter)

반응형

스프링이  SecurityContextHolder를 유지하기 위한 방법중 하나는  session을 활용하여  SecurityContextHolder를 유지하는 방법이다 .  지금까지 공부했던 formLogin 방식이 session 기반으로 동작하는 대표적인 로그인 방식이다 . 

 

 

SecurityContextHolder를 알아봤으니 이제 스프링이 SecurityContextHolder을 유지할 때 사용하는 session에 대해 알아보자 . 

 

ConcurrentSessionFilter

 

session은  톰캣과 같은 서블릿 컨테이너에서 제공하는 것이기 떄문에 스프링이 세션을 제어할 수는 없고 톰캣이 넘겨주는 세션을 스프링은 sessionInformation이라는 랩퍼객체를 만들어 SessionRegistry에서 관리한다.  톰캣의 세션과  SessionRegistry의 세션이 일치한다고는 할 수 없지만   요청이 들어왔을때  스프링에서 sessionInformation의 expired를 통해 해당 세션을 스프링 컨테이너로 받아 줄지 말지를 결정할 수 있기 때문에 세션을 관리하는 효과를 낼 수 있다 해당 역할을 하는 필터가 ConcurrentSessionFilter이다.

 

  private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
			throws IOException, ServletException {
		HttpSession session = request.getSession(false);
		if (session != null) {
			SessionInformation info = this.sessionRegistry.getSessionInformation(session.getId());
			if (info != null) {
				if (info.isExpired()) {
					// Expired - abort processing
					this.logger.debug(LogMessage
							.of(() -> "Requested session ID " + request.getRequestedSessionId() + " has expired."));
					doLogout(request, response);
					this.sessionInformationExpiredStrategy
							.onExpiredSessionDetected(new SessionInformationExpiredEvent(info, request, response));
					return;
				}
				// Non-expired - update last request date/time
				this.sessionRegistry.refreshLastRequest(info.getSessionId());
			}
		}
		chain.doFilter(request, response);
	}

 

ConcurrentSessionFilter의 doFilter메서드를 살펴보면 session을 가져와  sessionRegistry에서 sessionInfomation을 얻어내고  

sessionInfomation의 expired가 true인지 false인지 확인하여  true라면  세션을 expired시키고 sessionRegistry에서 해당 세션을 정리한다.   그림으로 보면 아래와 같다. 

 

 

 

 

 

sessionRegistry

 

세션들을 저장 해놓는 역할을 한다. 

 

principals 과 sessionIds 를 필드로 갖고 있으며 구현체로는 SessionRegistryImpl 클래스가 있다.

 

principals는 Map의 형태로  Principal 객체와 , Set형태의 세션 아이디를  갖으며

 

sessionIds는 세션 아이디 와 해당 세션아이디에 대한 SessionInfomation(위 그림의 SessionInfomation 테이블의 정보를 담는다)이 담긴다. 

 

 

 

 

 

 

SessionManagementFilter

 

 

세션을 확인하고 sessionInfomation을 확인하여 expired시키는 필터가  ConcurrentSessionFilter

 어떤 세션에 대해서 expired시키는지를 관리하는 필터는 SessionManagementFilter이다

SessionManagementFilter는 세션 생성 정책 , 세션 아이디 고정 설정 , 동시접근 허용 문제 , 세션 타임아웃 문제를 설정할 수 있다. 

SessionAuthenticationStrategy라는 인터페이스를 가지고 있는데 

 

SessionAuthenticationStrategy의 구현체에는 2가지 분류가 있다 . 

 

 

1. 동시 접속 문제 해결를 위한 구현체

 

ConcurrentSessionControlAuthenticationStrategy ,RegisterSessionAuthenticationStrategy

 

동시 접속에 대한 전략을 결정하는데 사용한다 , 만약 동시 접속을 한명 혹은 두명으로 제한하도록 설정하면 사용자가 초과될 경우  기존의 사용자를 expire시키든지 나중에 접근한 사용자를 expire 시키든지를 설정 할 수 있다.   

 

 

2.세션 고정 문제 해결을 위한 구현체

 

AbstractSessionFixationProtectionStrategy , ChangeSessionIdAuthenticationStrategy

 

한 사용자에 대해서 로그인시 세션을 고정시키거나  로그인 할때마다 새로운 세션을 부여하는 방식을 결정한다.  

세션을 고정시키면   유저1이 유저2의 브라우저에 악의적으로 자신이 부여 받은 세션을 심어놓게 되면  유저2가 로그인 할때 세션은 

유저 1이 심어 놓은 세션이된다. 그렇게 되면 유저1과 2은 같은 세션을 갖기 때문에 유저2가 로그인을 하면 유저 1도 로그인된 상태로 

사이트에 접근할 수 있게 된다 . 그렇기 떄문에 세션을 고정하는 방식보다는 로그인마다 세션을 부여하는 방식을 사용한다. 

 

 

HttpSecurity에 sessionManagement()로 session관리에 대한 strategy들을 설정할 수 있다. 

 

 

   @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests(request->
                    request.antMatchers("/").permitAll()
                            .anyRequest().authenticated()
                )
                .formLogin(login->
                        login.loginPage("/login")
                        .permitAll()
                        .defaultSuccessUrl("/", false)
                        .failureUrl("/login-error")
                )
                .logout(logout->
                        logout.logoutSuccessUrl("/"))
                .exceptionHandling(error->
                        error.accessDeniedPage("/access-denied")
                )
                .rememberMe(r->r
                        .rememberMeServices(rememberMeServices())
                )
                //세션 관리에 대한 설정을 위한 메서드            
                 .sessionManagement(
                        s->s.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                                .sessionFixation(sf ->sf.changeSessionId())
                                .maximumSessions(1)
                            .maxSessionsPreventsLogin(true)
                            .expiredUrl("/session-expired")
                )
                ;
    }

 

.sessionManagement()를 보면 

 

 

1. 세션 생성을 위한 속성

 

sessionCreationPolicy는 세션의 생성 정책을 정하는데 사용한다 , 

 

ALWAYS, - 항상 세션을 생성한다. 

IF_REQUIRED  - 필요할때만 세션을 생성한다. 

NEVER - 스프링 시큐리티에서 세션을 생성하지 않지만 세션이 존재한다면 사용. 

STATELESS  - 모든 요청에 대해 세션을 생성하지 않는다. 

 

보통 ALWAYS , STATELESS 두가지로 설정한다고한다.  

ALWAYS는 세션이 필요한 상황에서 설정하고 

STATELESS는 세션이 필요없는 ajax로그인 JWT로그인 같은 곳에서 설정한다. 

 

위의 4개가 Enum값으로 있다

 

sessionFixation() 은 세션의 생성 방식에 대해 정의한다 . 

 

changeSession() : 인증할때마다 새로운 세션아이디를 발급한다 . 다른 모든 세션 속성은 유지된다.

none() : 이전에 만들어진 세션이 사용된다 (세션이 고정된다)

newSession() :  아예 새로운 세션이 만들어진다.

migrateSession() : 디폴트 속성이며  인증시에 세션이 생성되고 이전 세션이 무효화된다 ,  이전 세션의 속성이 복사된다.

 

위에서 말했듯이 보안상 문제가 있어 none은 사용하지 않는다 .

 

 

2. 동시 접속 문제 해결를 위한 속성

 

maximumSession()은 한 유저가 갖을 수 있는 세션의 갯수를 정한다.

 

maxSessionsPreventsLogin()은 사용자 수를 초과했을시 상황을 정의한다 

true일 경우 초과된 사용자에게 로그인을 허용한다. 때문에 이전 사용자가 세션이 만료된다. 

false일 경우 초과된 사용자에게 로그인을 허용하지 않는다. 떄문에 초과된 사용자는 로그인 할 수 없게 된다. 

 

expiredUrl() 은 세션 만료시에 보낼 페이지를 정한다. 

 

 

 

반응형