본문 바로가기

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


Spring Security

스프링시큐리티 공부 13 - 세션관리 (세션 모니터링)

반응형

이전 시간에 공부한 것을 토대로  현재 세션을 리스트를 출력해주고 세션을 expire시킬 수 있는 세션 모니터링 페이지를 만들어보자  .

 

 

먼저 thymeleaf로 세션 리스트 출력과 세션 종료를 위해 세션 아이디를 전송해 주는 폼을 갖는 뷰와 , 

 세션 종료시 보낼 session-expired 뷰를 준비했다. 

 

 

sessionList.html

 

테이블에서 서버에서 보낸 sesstionList를 뿌려 주고 있다. form 에서는 session 종료를 위해 session Id를 서버로 전송한다. 

<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <link rel="stylesheet" th:href="@{/css/bootstrap.css}">
    <link rel="stylesheet" th:href="@{/css/login.css}">
</head>
<body>
<div class="container">
  <table class="table table-success">
    <tr>
        <th>유저</th>
        <th>sessionId</th>
        <th>마지막 요청 시간</th>
        <th>세션 유효 여부</th>
    </tr>
    <th:block  th:each="user:${sessionList}" >
        <th:block th:each="sessionInfo,i:${user.sessions}" th:object="${sessionInfo}">
            <tr th:if="${i.index == 0}" th:rowspan="${user.count}" >
                <td th:text="${user.username}">username</td>
                <td th:text="*{sessionId}">sessionId</td>
                <td th:text="*{#dates.format(lastRequest , 'yyyy-MM-dd HH:mm:ss')}">time</td>
                <td>
                    <form th:action="@{/session/expire}" method="post">
                        <input type="hidden" name="sessionId" th:value="*{sessionId}">
                        <button class="btn btn-sm btn-success">강제 종료</button>
                    </form>
                </td>
            </tr>

        </th:block>
    </th:block>
  </table>
</div>
</body>

 

 

sessionExpired.html

 

간단하게 세션 만료 텍스트와 메인페이지로 보내는 링크가 있다.

 

<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <link rel="stylesheet" th:href="@{/css/bootstrap.css}">
    <link rel="stylesheet" th:href="@{/css/index.css}">
</head>
<body>

<div class="container center-contents">
    <div class="row">
        <h1 class="title display-5 text-danger"> 접근 권한이 없습니다. </h1>
    </div>
    <div class="links">
        <h2 class="text text-danger">세션이 만료되었습니다</h2>
        <div class="link">
            <a href="/"> 메인 </a>
        </div>

    </div>
</div>
<script th:src="@{/js/bootstrap.js}" />
</body>
</html>

 

 

 

다음으론 요청을  SessionRegistry에서 가져온 정보를 담을 dto 객체를 만들었다 

 

UserSession 클래스는 SessionRegistryImpl의 principals를 담고 

SessionInfo 클래스는 SessionRegistryImpl의  sessionIds를 담는 클래스이다 . 

 

//UserSession

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class UserSession {

    private String username;
    private List<SessionInfo> sessions;

    public int getCount(){
        return sessions.size();
    }
}

//SessionInfo

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class SessionInfo {
    
    private String sessionId;
    private Principal principal;
    private Date lastRequest;
    private boolean expired;
}

 

 

 

SessionController

 

요청을 받을 controller를 만들었다.   SessionRegistry를 통해 현재 session들의 정보를 가져와야 하기 떄문에 

 

bean으로  선언하고 주입받았다 

 

@Bean
SessionRegistry sessionRegistry(){
   return new SessionRegistryImpl();
}

 

 

@Controller
@RequiredArgsConstructor
public class SessionController {

    private final SessionRegistry registry;


    @GetMapping("/session-list")
    public String sessions(Model model){


        List<UserSession> userSessions = registry.getAllPrincipals().stream()
                .map(p -> UserSession.builder()
                        .username(((SpUser)p).getUsername())
                        .sessions(registry.getAllSessions(p,false)
                                .stream().map(si ->SessionInfo.builder()
                                        .lastRequest(si.getLastRequest())
                                        .sessionId(si.getSessionId())
                                        .build()
                                ).collect(Collectors.toList()))
                        .build())
                .collect(Collectors.toList());

        model.addAttribute("sessionList",userSessions);

        return "sessionList";
    }

    @PostMapping("/session/expire")
    public String expireSession(@RequestParam String sessionId){
        SessionInformation session = registry.getSessionInformation(sessionId);
        if(!session.isExpired() == true){
            session.expireNow();
        }
        return "redirect:/session-list";
    }

    @GetMapping("/session-expired")
    public String sessionExpired(){
        return "sessionExpired";
    }

}

 

 

    @GetMapping("/session-list")
    public String sessions(Model model){


        List<UserSession> userSessions = registry.getAllPrincipals().stream()
                .map(p -> UserSession.builder()
                        .username(((SpUser)p).getUsername())
                        .sessions(registry.getAllSessions(p,false)
                                .stream().map(si ->SessionInfo.builder()
                                        .lastRequest(si.getLastRequest())
                                        .sessionId(si.getSessionId())
                                        .build()
                                ).collect(Collectors.toList()))
                        .build())
                .collect(Collectors.toList());

        model.addAttribute("sessionList",userSessions);

        return "sessionList";
    }

 

먼저 list를 출력해주는 sessions 메서드를 살펴보자 registry를 통해 등록되있는 principals를 가져오고 UserSession을 생성하여 맵핑해 주고 있다.  이때 UserSession의 sessions 필드의 타입은 List<SessionInfo> 이기 때문에

registry.getAllSessions(Object principal , 만료된 세션도 포함시키는지 여부 ) 로 해당 사용자에 대한 sessionInfo를 가져와 SessionInfo dto 클래스에 맵핑해주고 있다 . 

 

맵핑된 userSession 리스트를  모델 객체에 담아 뷰로 넘기고있다.  

 

 

세션 강제종료 시키는 요청이다

 

 @PostMapping("/session/expire")
    public String expireSession(@RequestParam String sessionId){
        SessionInformation session = registry.getSessionInformation(sessionId);
        if(!session.isExpired() == true){
            session.expireNow();
        }
        return "redirect:/session-list";
    }

 

레지스트리에서 sessionInformation을 가져와 expire 시키고 있다 다음 요청이 있을 시에 ConcurrentSessionFilter에 의해 

isExpired값이 true로 바뀐것을 확인되고 세션이 expire 될 것이다. 

 

 

securityConfig에는 아래와 같이 설정했다.  

 

 

한 유저가 갖을 수 있는 세션 수를 1로  설정  , 세션이 한개 이상일 경우  초과된 사용자의 로그인을 막도록 해놨다.

세션이 만료될시 "/session-expired" 로 보낸다.  

 

 

서버를 돌려서 확인해보자

등록해 둔 user1으로 로그인 후 /session-list로 요청해 보니 세션 출력이 잘되었다

 

 

구글 시크릿 모드로 창을 띄우면 다른 브라우저가 다른 세션는 일반 모드의 창과 다른 세션을 사용한다 . 

 

시크릿 창을 띄우고  user2로 로그인 해봤다

 

 

 

왼쪽이 일반모드고 오른쪽이 시크릿 창이다  시크릿 창에 세션이 2개가 잡히는 것을 알 수 있다. 

 

동시 접속을 확인하기 위해 시크릿창에서 user1으로 로그인 해봤다 

 

 

초과사용자의 로그인을 막도록 설정해 두었기 때문에  로그인이 불가능하다 . 

 

세션을 강제 종료 시켜봤다 세션이 만료되어 /session-expried 경로로 보내졌다.

 

maximumSession을 2로 늘려봤다 . 

한 유저가 2개의 세션을 갖을 수 있게 되었다. 

 

반응형