카테고리 없음

스프링-시큐리티 SessionManagementFilter

yoon9 2022. 12. 18. 15:26

UsernamePasswordAuthenticationFilter다음으로 이번장을 다시 또 정리해본다...

 

이제 Authentication을 다 만들었다

SessionManagementFilter를 해보자

 

AbstractAuthenticationProcessiongFilter에서 sessionStrategy의 onAuthentication이 인증받은 객체로 실행한다.

로그인 인증 시도 시에 작동을 한다.

사용자 세션전략을 세우는 것이라고 보면 될 것이다.

 

시큐리티에서 세우는 세션전략은 4가지 이다.

  1. 세션관리 : 인증 시 사용자의 세션 정보를 등록, 조회, 삭제 등의 세션이력을 관리
  2. 동시적 세션 제어 : 동일 계정으로 접속이 허용되는 최대 세션수를 제한
  3. 세션 고정 보호 : 인증 할 때 마다 세션 쿠키를 새로 발급하여 공격자의 쿠키 조작을 방지
  4. 세션 생성 정책 : Always, If_Required, Never, Stateless

 

예를 들어보면 대부분의 웹사이트들은 중복로그인에 대해 막아놨을 것이다 이걸보면

모바일로 로그인하고 다시 pc에서 로그인할려고 보면 갑자기 이미 로그인 중이라던지 그런 알림이 뜰 것이다.

이런 것들을 설정하고 제어하는 전략이라고 보면 될 것같다.

 

쿠키/세션으로 개발을 하기 때문에 톰켓에서 주는 session은 스프링 내부에서는 제어를 할 수가 없다.

그래서 스프링 시큐리티에서는 이것을 나름 세션을 제어하는 전략을 세운 것이다.

public interface SessionAuthenticationStrategy {
    void onAuthentication(Authentication authentication, HttpServletRequest request, HttpServletResponse response) throws SessionAuthenticationException;
}

 

위에는 sessionAuthenticationStategy 인터페이스인데 

총 8개가 이 인터페이스를 구현하고 있다.

구현 객체 중에는 CompositeSessionAuthenticationStrategy를 유심히 봐보자.

 sessionStrategy의 onAuthentication이 실행되면 CompositeSessionAuthenticationStrategy로 넘어서가서 onAuthentication가 실행되는것이다.

 

public class CompositeSessionAuthenticationStrategy implements SessionAuthenticationStrategy {
    private final Log logger = LogFactory.getLog(this.getClass());
    private final List<SessionAuthenticationStrategy> delegateStrategies;

    public CompositeSessionAuthenticationStrategy(List<SessionAuthenticationStrategy> delegateStrategies) {
        Assert.notEmpty(delegateStrategies, "delegateStrategies cannot be null or empty");
        Iterator var2 = delegateStrategies.iterator();

        while(var2.hasNext()) {
            SessionAuthenticationStrategy strategy = (SessionAuthenticationStrategy)var2.next();
            Assert.notNull(strategy, () -> {
                return "delegateStrategies cannot contain null entires. Got " + delegateStrategies;
            });
        }

        this.delegateStrategies = delegateStrategies;
    }

    public void onAuthentication(Authentication authentication, HttpServletRequest request, HttpServletResponse response) throws SessionAuthenticationException {
        int currentPosition = 0;
        int size = this.delegateStrategies.size();

        SessionAuthenticationStrategy delegate;
        for(Iterator var6 = this.delegateStrategies.iterator(); var6.hasNext(); delegate.onAuthentication(authentication, request, response)) {
            delegate = (SessionAuthenticationStrategy)var6.next();
            if (this.logger.isTraceEnabled()) {
                Log var10000 = this.logger;
                String var10002 = delegate.getClass().getSimpleName();
                ++currentPosition;
                var10000.trace(LogMessage.format("Preparing session with %s (%d/%d)", var10002, currentPosition, size));
            }
        }

    }

    public String toString() {
        return this.getClass().getName() + " [delegateStrategies = " + this.delegateStrategies + "]";
    }
}

CompositeSessionAuthenticationStrategy

 

sessionAuthenticationStrategy타입의 sessionStrategy들이 4개가 들어가 있다.

그럼 이제 for문을 돌리면서 List안에 있는 sessionStrategy들을 인증한 Authentication객체와 request,response를 넣어서 

authentication을 실행한다.

각각 하나씩 돌려서 실행시키는 것이다.

 

자 그럼 처음으로 ConcurrentSessionControlAuthenticationStrategy가 실행된다.

ConcurrentSessionControlAuthenticationStrategy
public void onAuthentication(Authentication authentication, HttpServletRequest request, HttpServletResponse response) {
    int allowedSessions = this.getMaximumSessionsForThisUser(authentication);
    if (allowedSessions != -1) {
        List<SessionInformation> sessions = this.sessionRegistry.getAllSessions(authentication.getPrincipal(), false);
        int sessionCount = sessions.size();
        if (sessionCount >= allowedSessions) {
            if (sessionCount == allowedSessions) {
                HttpSession session = request.getSession(false);
                if (session != null) {
                    Iterator var8 = sessions.iterator();

                    while(var8.hasNext()) {
                        SessionInformation si = (SessionInformation)var8.next();
                        if (si.getSessionId().equals(session.getId())) {
                            return;
                        }
                    }
                }
            }

            this.allowableSessionsExceeded(sessions, allowedSessions, this.sessionRegistry);
        }
    }
}

여기서 동시적 세션 제어 정책을 세운다.

Authentication의 세션개수를 받아온다. 나는 설정 1로 해놨다. 그니까 즉 1명만 되고 중복로그인을 설정해놓지 않은 것이다.

이러한 설정들은

     http.sessionManagement()
                .sessionFixation()
                .changeSessionId()
                .maximumSessions(1)
                .maxSessionsPreventsLogin(false)
                .sessionRegistry(sessionRegistry())
                .expiredUrl("/signin");

이렇게 해놨다.

차례대로 보면

  • maximumSessions(1)
    • 1명의 로그인만 허용.
    • -1일 경우 제한 없음..
  • .maxSessionsPreventsLogin(false) 
    • 일단 첫번째 로그인한 유저말고 두번째로 다른 쿠키/세션을 들고 있는 유저가 같은 아이디로 로그인을 시도하여 성공했을 경우 첫번째 유저의 세션을 죽이고 두번째 유저가 세션생성 -> false;
    • 두번째 로그인을 한 유저를 세션을 아예 못들어오게 막기-> true;
  • .sessionRegistry(sessionRegistry())
    • 세션의 생성 및 조회 등등을 위하여 설정.
  •  .expiredUrl("/signin");
    • 만료일 경우 이동 url
  •    .sessionFixation().changeSessionId()
    • 매번 sessoinId를 바꿔준다. 쿠키/세션 환경으로 개발을 하였기때문에 sessionId를 매번 바꿔주지않고 고정으로 한다면 로그인에 성공한 유저인 경우에 sessionId가 탈취당했을 경우 공격자가 아주 편하게..sessionId를 이용하여서 사용자의 정보를 탈취하며... 난리가 난다..

ConcurrentSessionControlAuthenticationStrategy를 이제 보면

살짝 이해가 갈 것이다 SessionInfomation을 조회를 하여서 작업을 시작한다.

public class SessionInformation implements Serializable {
    private static final long serialVersionUID = 570L;
    private Date lastRequest;
    private final Object principal;
    private final String sessionId;
    private boolean expired = false;

    public SessionInformation(Object principal, String sessionId, Date lastRequest) {
        Assert.notNull(principal, "Principal required");
        Assert.hasText(sessionId, "SessionId required");
        Assert.notNull(lastRequest, "LastRequest required");
        this.principal = principal;
        this.sessionId = sessionId;
        this.lastRequest = lastRequest;
    }

    public void expireNow() {
        this.expired = true;
    }

    public Date getLastRequest() {
        return this.lastRequest;
    }

    public Object getPrincipal() {
        return this.principal;
    }

    public String getSessionId() {
        return this.sessionId;
    }

    public boolean isExpired() {
        return this.expired;
    }

    public void refreshLastRequest() {
        this.lastRequest = new Date();
    }
}

 

sessionInfomation의 구조이다.

sessionId와 인증객체에 대해 구분을 한다.

그럼 sessionRegistry을 이용하여 해당 요청의 데이터로 세션을 가져오고

maximumSessions(1) 와 sessionSize를 비교한다.

처음 로그인 시도일 경우에는 0일 것이다.

그럼 이제 내가 세운 전략이랑 위반되지 않기 때문에 return을 해준다.

여기서 그럼 다시 두번째 사용자가 같은 username으로 로그인을 시도한다고 가정을 해보자.

그러면 이제 sessionSize가 1일 것이다. 나는 전략을 maxSessionsPreventsLogin(false) 로 두었기 때문에 

this.allowableSessionsExceeded(sessions, allowedSessions, this.sessionRegistry); 가 실행된다.

ConcurrentSessionControlAuthenticationStrategy
protected void allowableSessionsExceeded(List<SessionInformation> sessions, int allowableSessions, SessionRegistry registry) throws SessionAuthenticationException {
    if (!this.exceptionIfMaximumExceeded && sessions != null) {
        sessions.sort(Comparator.comparing(SessionInformation::getLastRequest));
        int maximumSessionsExceededBy = sessions.size() - allowableSessions + 1;
        List<SessionInformation> sessionsToBeExpired = sessions.subList(0, maximumSessionsExceededBy);
        Iterator var6 = sessionsToBeExpired.iterator();

        while(var6.hasNext()) {
            SessionInformation session = (SessionInformation)var6.next();
            session.expireNow();
        }

    } else {
        throw new SessionAuthenticationException(this.messages.getMessage("ConcurrentSessionControlAuthenticationStrategy.exceededAllowed", new Object[]{allowableSessions}, "Maximum sessions of {0} for this principal exceeded"));
    }
}

 

그럼 위에 와 같이 실행되면서 전략에 맞게 첫번째에 로그인한 유저에 대해 sessionId 참조하여서 expiredNow()를 찍어준다.

이게 무엇이냐면 나중에 나올 filter에서 ConcurrentSessionFilter가 sessionInformaion에 대해 expire이 찍혀 있나 확인을 하여서 스프링내에 세션을 풀리게 한다. 위에 말했다 시피 스프링이 톰캣세션에 대해서는 조작을 할 수 없으니..스프링이 자체적으로 로그인을 막는 것이다.(참 머리 좋게 잘 만든거 같다.) 

 

다음으로는 ChangeSessionIdAuthenticationStrategy가 실행된다. 나는 매번 세션Id를 변경하는 정책을 세웠기 때문에 

현재 있는 인증객체의 sessionID를 변경해준다.

AbstractSessionFixationProtectionStrategy
public void onAuthentication(Authentication authentication, HttpServletRequest request, HttpServletResponse response) {
    boolean hadSessionAlready = request.getSession(false) != null;
    if (hadSessionAlready || this.alwaysCreateSession) {
        HttpSession session = request.getSession();
        if (hadSessionAlready && request.isRequestedSessionIdValid()) {
            Object mutex = WebUtils.getSessionMutex(session);
            String originalSessionId;
            String newSessionId;
            synchronized(mutex) {
                originalSessionId = session.getId();
                session = this.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));
            }

            this.onSessionChange(originalSessionId, session, authentication);
        }

    }
}

 

그러고 이제는 다음 Strategy인 RegisterSessionAuthStrategy로  넘어간다.

public class RegisterSessionAuthenticationStrategy implements SessionAuthenticationStrategy {
    private final SessionRegistry sessionRegistry;

    public RegisterSessionAuthenticationStrategy(SessionRegistry sessionRegistry) {
        Assert.notNull(sessionRegistry, "The sessionRegistry cannot be null");
        this.sessionRegistry = sessionRegistry;
    }

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

새로운세션을 등록하는 역할을 한다. 위에 말했다시피 스프링 시큐리티 자체의 session을 인증객체와 요청된 sessionId에 대해 저장하고 조회,등록,삭제등등의 역할을 한다. 나는 redis에서 세션을 관리하기 때문에 SpringSessionBackedSessionRegistry으로 등록하였다.

 

그러고 나서는 마지막으로 csrfTokenFilter가 작동을 하는데 csrf를 방지하기 위해서 작동을 한다.

계속 csrfToken을 갱신 및 확인을 하는 것을 진행한다.

 

이만 끝...