상세 컨텐츠

본문 제목

[SPRING SECURITY] SessionRegistry 이용하기

JAVA/SPRING

by ranlan 2022. 11. 20. 23:22

본문

728x90

스프링 시큐리티 필터로 제어불가능한 로그인 상황이 있었어서 직접 SessionRegistry를 이용해 세션을 관리하였다.

단순하게 동일한 아이디로 세션이 존재하는지 확인하고 없으면 새로운 세션 추가, 있으면 기존에 있던 세션을 만료시키는 기능을 구현하였다.

찾아보니 SessionRegistry가 아닌 세션정보를 담은 리스트나 맵 객체를 만들어서 구현하는 방법도 있었다.

 

암튼 이 과정에서 알게된 두 객체

  • SessionInformation
  • SessionRegistry

 

** 공부하거나 업무할수록 블로그도 많이 읽긴 하는데 가장 정보를 많이 얻고 찾아보게 되는건 역시 공식 문서인듯 **

 

 

SessionInformation

https://docs.spring.io/spring-security/site/docs/3.1.x/apidocs/org/springframework/security/core/session/SessionInformation.html

 

SessionInformation (Spring Security 3.1.7.RELEASE API)

Represents a record of a session within the Spring Security framework. This is primarily used for concurrent session support. Sessions have three states: active, expired, and destroyed. A session can that is invalidated by session.invalidate() or via Servl

docs.spring.io

public void expireNow();
public Date getLastRequest();
public Object getPrincipal();
public String getSessionId();
public boolean isExpired();

/**
 * Refreshes the internal lastRequest to the current date and time.
 */
public void refreshLastRequest() {
    this.lastRequest = new Date();
}

스프링 시큐리티에서 사용하는 세션 관리를 위한 세션 래퍼 객체이다. 

 

 

SessionRegistry

https://docs.spring.io/spring-security/site/docs/4.0.x/apidocs/org/springframework/security/core/session/SessionRegistry.html

 

SessionRegistry (Spring Security 4.0.4.RELEASE API)

getAllSessions List  getAllSessions(Object principal, boolean includeExpiredSessions) Obtains all the known sessions for the specified principal. Sessions that have been destroyed are not returned. Sessions that have expired may be returned, depending o

docs.spring.io

세션 객체 SessionInformation이 저장되어있는 곳으로 세션을 등록, 조회, 만료 등 세션 관리에 대한 기능을 지닌다.

📄 빈으로 등록

<beans:bean id="sessionRegistry" class="org.springframework.security.core.session.SessionRegistryImpl" />

📄 주입

@Autowired
private SessionRegistry sessionRegistry;

 

SessionRegistry는 인터페이스이고 이를 구현한 SessionRegistryImpl에서 주요 메서드를 파악할 수 있다.

 

🛠 getAllPrincipals()

아직도 정확한건 모르겠지만 암튼 모든 Principal을 반환하는 메서드

@Override
public List<Object> getAllPrincipals() {
    return new ArrayList<>(this.principals.keySet());
}

List<Object>로 반환되기 때문에 사용할 때 형변환이 맘대로 안되서 골치가 아팠던 기억

 

🛠 getAllSessions(Object principal, boolean includeExpiredSessions)

굉장히 내 머리를 아프게했던 메서드... 이 메서드가 뭘 가져오고 어떤 인자를 넘겨줘야하는지 알아내는데 오랜 시간이 걸렸다..(이 포스팅을 하게된 이유)

특정 Principal 객체(인증객체)로 존재하는 모든 SessionInformation 리스트 반환

@Override
public List<SessionInformation> getAllSessions(Object principal, boolean includeExpiredSessions) {
    Set<String> sessionsUsedByPrincipal = this.principals.get(principal);
    if (sessionsUsedByPrincipal == null) {
        return Collections.emptyList();
    }
    List<SessionInformation> list = new ArrayList<>(sessionsUsedByPrincipal.size());
    for (String sessionId : sessionsUsedByPrincipal) {
        SessionInformation sessionInformation = getSessionInformation(sessionId);
        if (sessionInformation == null) {
            continue;
        }
        if (includeExpiredSessions || !sessionInformation.isExpired()) {
            list.add(sessionInformation);
        }
    }
    return list;
}

중요한건 모든 세션이 아니라 매개변수로 받은 인증객체를 지닌 세션정보만 반환한다는것!! 

따라서 동시세션 제어에서 이 메서드를 이용하고 싶다면 매개변수로 접속을 제어하고자하는 인증객체의 principal을 넘기는게 중요하다.

 

여기서 알아두면 좋은 Authentication

public interface Authentication extends Principal, Serializable {
    Collection<? extends GrantedAuthority> getAuthorities();
    Object getCredentials(); 
    Object getDetails(); 
    Object getPrincipal();
    boolean isAuthenticated(); 
    void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
Object principal = Authentication.getPrincipal();

 

🛠 getSessionInformation(String sessionId)

매개변수로 받은 세션 아이디를 가진 SessionInformation 반환

@Override
public SessionInformation getSessionInformation(String sessionId) {
    Assert.hasText(sessionId, "SessionId required as per interface contract");
    return this.sessionIds.get(sessionId);
}

 

🛠 registerNewSession(String sessionId, Object principal)

매개변수로 받은 sessionId와 인증 객체로 SessionRegistry에 세션 등록

@Override
public void registerNewSession(String sessionId, Object principal) {
    Assert.hasText(sessionId, "SessionId required as per interface contract");
    Assert.notNull(principal, "Principal required as per interface contract");
    if (getSessionInformation(sessionId) != null) {
        removeSessionInformation(sessionId);
    }
    if (this.logger.isDebugEnabled()) {
        this.logger.debug(LogMessage.format("Registering session %s, for principal %s", sessionId, principal));
    }
    this.sessionIds.put(sessionId, new SessionInformation(principal, sessionId, new Date()));
    this.principals.compute(principal, (key, sessionsUsedByPrincipal) -> {
        if (sessionsUsedByPrincipal == null) {
            sessionsUsedByPrincipal = new CopyOnWriteArraySet<>();
        }
        sessionsUsedByPrincipal.add(sessionId);
        this.logger.trace(LogMessage.format("Sessions used by '%s' : %s", principal, sessionsUsedByPrincipal));
        return sessionsUsedByPrincipal;
    });
}

 

🛠 removeSessionInformation(String sessionId)

해당 세션 아이디로 된 세션 삭제

@Override
public void removeSessionInformation(String sessionId) {
    Assert.hasText(sessionId, "SessionId required as per interface contract");
    SessionInformation info = getSessionInformation(sessionId);
    if (info == null) {
        return;
    }
    if (this.logger.isTraceEnabled()) {
        this.logger.debug("Removing session " + sessionId + " from set of registered sessions");
    }
    this.sessionIds.remove(sessionId);
    this.principals.computeIfPresent(info.getPrincipal(), (key, sessionsUsedByPrincipal) -> {
        this.logger.debug(
                LogMessage.format("Removing session %s from principal's set of registered sessions", sessionId));
        sessionsUsedByPrincipal.remove(sessionId);
        if (sessionsUsedByPrincipal.isEmpty()) {
            // No need to keep object in principals Map anymore
            this.logger.debug(LogMessage.format("Removing principal %s from registry", info.getPrincipal()));
            sessionsUsedByPrincipal = null;
        }
        this.logger.trace(
                LogMessage.format("Sessions used by '%s' : %s", info.getPrincipal(), sessionsUsedByPrincipal));
        return sessionsUsedByPrincipal;
    });
}

** 정확하진 않지만 남는 로그 생긴걸로 봐서는(필터를 통한 SessionRegistry 로그와 동일) 물리적인 만료까지 되는 것 같음

 

 

SessionRegistry 이용해서 동시접속 제어 구현

정확하게 코드를 작성할 순 없지만 대충 참고한 블로그의 코드와 비슷하게 구현하면 된다.

List sessionList = sessionRegistry.getAllSessions(auth.getPrincipal(), false); // 해당 인증객체를 지닌 세션정보들
for(SessionInformation session : sessionList){
    // 아이디가 동일할 때(세션 만료가 필요할 떄)
    session.expireNow(); 
}

 

 


[[Spring-Security] 계정 정지 기능 구현 - 2] https://dev-gyus.github.io/spring/2021/03/18/SpringSecurity-AccountLock-2.html

[토리맘의 한글라이징 프로젝트 | Spring Security Authentication] https://godekdls.github.io/Spring%20Security/authentication/

 

728x90

관련글 더보기

댓글 영역