이전 시간에 공부한 것을 토대로 현재 세션을 리스트를 출력해주고 세션을 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개의 세션을 갖을 수 있게 되었다.
'Spring Security' 카테고리의 다른 글
스프링시큐리티 공부 15 - 권한이란? (0) | 2021.07.08 |
---|---|
스프링시큐리티 공부 14 - FilterSecurityInterceptor 와 ExceptionTranslationFilter (0) | 2021.07.07 |
스프링시큐리티 공부 12 - 세션관리 (ConcurrentSessionFilter) (0) | 2021.07.06 |
스프링시큐리티 공부 11 - 로그인 유지 (session과 remberme cookie) (0) | 2021.07.06 |
스프링시큐리티 공부 9 - multi filter chain 구현 (0) | 2021.07.03 |