Spring Security - SessionManagementFilter & ConcurrentSessionFilter

목차

SessionManagementFilter

SessionManagementFilter 는 인증된 사용자에 대한 Session 관리Session 공격으로 부터의 보호 를 위한 Filter

  1. Session 관리
    • 인증시 사용자의 Session 등록, 조회, 삭제 등의 이력을 관리한다.
  2. 동시성 Session 제어
    • 동일 계정으로 접속하는 최대 Session 수를 설정한다.
  3. Session 고정 보호
    • 인증할때 마다 새로운 Session 쿠키를 새로 발급해 쿠키 조작을 방지한다.
  4. Session 생성 정책

SessionManagementFilter.java

private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
if (request.getAttribute(FILTER_APPLIED) != null) {
chain.doFilter(request, response);
return;
}

request.setAttribute(FILTER_APPLIED, Boolean.TRUE);

if (!this.securityContextRepository.containsContext(request)) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && !this.trustResolver.isAnonymous(authentication)) {
try {
// 최대 생성 개수를 넘어섰는지?, Session 고정 공격이 들어왔는지? 등
// Session에 대한 인증을 진행한다.
this.sessionAuthenticationStrategy.onAuthentication(authentication, request, response);
} catch (SessionAuthenticationException ex) {
this.logger.debug("SessionAuthenticationStrategy rejected the authentication object", ex);
SecurityContextHolder.clearContext();
this.failureHandler.onAuthenticationFailure(request, response, ex);
return;
}

this.securityContextRepository.saveContext(SecurityContextHolder.getContext(), request, response);
} else {
if (request.getRequestedSessionId() != null && !request.isRequestedSessionIdValid()) {
if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Request requested invalid session id %s",
request.getRequestedSessionId()));
}
if (this.invalidSessionStrategy != null) {
this.invalidSessionStrategy.onInvalidSessionDetected(request, response);
return;
}
}
}
}
chain.doFilter(request, response);
}

Session 관리를 위한 SessionAuthenticationStrategy

SessionAuthenticationStrategy 는 Session 이 존재하는지 확인하거나 Session 고정 공격에 대한 보호를 위해 Session Id를 변경한다.

SessionAuthenticationStrategy.java

public interface SessionAuthenticationStrategy {

void onAuthentication(Authentication authentication, HttpServletRequest request, HttpServletResponse response)
throws SessionAuthenticationException;

}

SessionAuthenticationStrategy 객체들을 관리하기 위한 - CompositeSessionAuthenticationStrategy

CompositeSessionAuthenticationStrategy 는 여러개의 SessionAuthenticationStrategy 를 갖고 요청에 대한 Session 인증 처리를 위임한다.

@Override
public void onAuthentication(Authentication authentication, HttpServletRequest request,
HttpServletResponse response) throws SessionAuthenticationException {
int currentPosition = 0;
int size = this.delegateStrategies.size();
for (SessionAuthenticationStrategy delegate : this.delegateStrategies) {
if (this.logger.isTraceEnabled()) {
this.logger.trace(LogMessage.format("Preparing session with %s (%d/%d)",
delegate.getClass().getSimpleName(), ++currentPosition, size));
}
delegate.onAuthentication(authentication, request, response);
}
}

Session 을 저장하기 위한 - RegisterSessionAuthenticationStrategy

RegisterSessionAuthenticationStrategy 는 인증이 성공적으로 끝난 후 사용자 Session을 저장한다.

@Override
public void onAuthentication(Authentication authentication, HttpServletRequest request,
HttpServletResponse response) {
this.sessionRegistry.registerNewSession(request.getSession().getId(), authentication.getPrincipal());
}

동시성 Session 제어를 위한 - ConcurrentSessionControlAuthenticationStrategy

ConcurrentSessionControlAuthenticationStrategy 은 한 사용자가 만들 수 있는 Session 이 최대 생성 개수 를 넘어갔는지 확인한다.

@Override
public void onAuthentication(Authentication authentication, HttpServletRequest request,
HttpServletResponse response) {
int allowedSessions = getMaximumSessionsForThisUser(authentication);
if (allowedSessions == -1) {
// We permit unlimited logins
return;
}
List<SessionInformation> sessions = this.sessionRegistry.getAllSessions(authentication.getPrincipal(), false);
int sessionCount = sessions.size();
if (sessionCount < allowedSessions) {
// They haven't got too many login sessions running at present
return;
}

// 해당 사용자로 만들어진 Session의 개수와 허용된 Session 생성 개수를 비교한다.
if (sessionCount == allowedSessions) {
HttpSession session = request.getSession(false);
if (session != null) {
for (SessionInformation si : sessions) {
if (si.getSessionId().equals(session.getId())) {
return;
}
}
}
}

// Session이 최대치 보다 많이 생성된 경우에 대한 처리를 진행한다.
allowableSessionsExceeded(sessions, allowedSessions, this.sessionRegistry);
}

allowableSessionsExceeded 메소드는 현재 사용자의 인증에 대한 예외 처리를 진행하는 방법
이전 사용자 Session 정보를 만료하는 방법 이 있다.

ConcurrentSessionControlAuthenticationStrategy.java

protected void allowableSessionsExceeded(List<SessionInformation> sessions, int allowableSessions,
SessionRegistry registry) throws SessionAuthenticationException {

// 현재 사용자가 인증을 못하도록 예외를 발생시킨다.
// Session이 허용된 개수보다 많이 만들어졌거나, Session 자체가 없을 경우 SessionAuthenticationException 을 발생시킨다.
if (this.exceptionIfMaximumExceeded || (sessions == null)) {
throw new SessionAuthenticationException(
this.messages.getMessage("ConcurrentSessionControlAuthenticationStrategy.exceededAllowed",
new Object[] { allowableSessions }, "Maximum sessions of {0} for this principal exceeded"));
}

// 이전 사용자 Session 정보를 만료 한다.
sessions.sort(Comparator.comparing(SessionInformation::getLastRequest));
int maximumSessionsExceededBy = sessions.size() - allowableSessions + 1;

// 첫번째 사용자 정보를 가져와 만료한다.
List<SessionInformation> sessionsToBeExpired = sessions.subList(0, maximumSessionsExceededBy);
for (SessionInformation session : sessionsToBeExpired) {
session.expireNow();
}
}

Session 고정 공격 보호를 위한 - AbstractSessionFixationProtectionStrategy

AbstractSessionFixationProtectionStrategySession 고정 공격 보호 공통적인 로직을 수행시키기 위해 만들어진 추상 Class

  • Session을 가져온다. 만일 Session이 없을 경우 새로 생성한다.
  • Session 고정에 대한 적절한 처리를 진행한다.(Session Id를 변경)
  • Session Id가 변경 됐는지 확인한다.
  • SessionFixationProtectionEvent 이벤트를 발생한다.

AbstractSessionFixationProtectionStrategy.java

@Override
public void onAuthentication(Authentication authentication, HttpServletRequest request,
HttpServletResponse response) {
// 만들어진 Session이 있으면 가져온다.
boolean hadSessionAlready = request.getSession(false) != null;
if (!hadSessionAlready && !this.alwaysCreateSession) {
return;
}

// Session을 생성한다.
HttpSession session = request.getSession();
if (hadSessionAlready && request.isRequestedSessionIdValid()) {
String originalSessionId;
String newSessionId;
Object mutex = WebUtils.getSessionMutex(session);
synchronized (mutex) {
originalSessionId = session.getId();
// Session 고정에 대한 적절한 처리를 진행한다.(Sessio Id를 변경)
session = applySessionFixation(request);
newSessionId = session.getId();
}

if (originalSessionId.equals(newSessionId)) {
this.logger.warn("Your servlet container did not change the session ID when a new session "
+ "was created. You will not be adequately protected against session-fixation attacks");
} else {
if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Changed session id from %s", originalSessionId));
}
}
onSessionChange(originalSessionId, session, authentication);
}
}

Session Id를 변경 - ChangeSessionIdAuthenticationStrategy

ChangeSessionIdAuthenticationStrategy 는 AbstractSessionFixationProtectionStrategy 상속한 Class 다
ChangeSessionIdAuthenticationStrategy 는 Session 고정 공격 보호를 위한 방법으로 Session Id를 변경 한다.

ChangeSessionIdAuthenticationStrategy.java

public final class ChangeSessionIdAuthenticationStrategy extends AbstractSessionFixationProtectionStrategy {

@Override
HttpSession applySessionFixation(HttpServletRequest request) {
request.changeSessionId();
return request.getSession();
}
}

ConcurrentSessionFilter - 동시성 Session 제어를 위한 Filter

ConcurrentSessionFilterSessionManagementFilter 와 함께 동시적 Session 제어를 위한 Filter

  • 매 요청 마다 현재 사용자의 Session 만료 여부를 체크
  • Session이 만료 됐을 경우 즉시 만료 처리한다.
    • Logout 처리
    • Session 만료 정보에 대한 이벤트를 발생시킨다.

ConcurrentSessionFilter.java

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) {
// Session이 반료 됐는지 확인한다.
if (info.isExpired()) {
this.logger.debug(LogMessage
.of(() -> "Requested session ID " + request.getRequestedSessionId() + " has expired."));
// Session이 만료 됐을 경우 Logout 처리한다.
doLogout(request, response);
this.sessionInformationExpiredStrategy
.onExpiredSessionDetected(new SessionInformationExpiredEvent(info, request, response));
return;
}
// Session이 만료되지 않았을 경우 Session 시간을 갱신한다.
this.sessionRegistry.refreshLastRequest(info.getSessionId());
}
}
chain.doFilter(request, response);
}
Share