Security Context Holder를 유지하기 위해 Spring에서 세션 자주 사용
서버는 기본적으로 사용자를 보면서 판단할 수 없음
-> 서버는 로그인을 통해 요청을 보낸 사용자를 구분
하지만 모든 요청에 아이디/패스워드를 물어볼 수는 없다
-> 그래서 토큰을 발급하고, 세션에는 토큰을 저장해 놓고 세션이 유지되는 동안, 혹은 remember-me 토큰이 있다면 해당 토큰이 살아있는 동안 로그인 없이 해당 토큰만으로 사용자를 인증하고 요청을 처리해 줌
악의적으로 정보를 취하고자 하는 사람들(해커)은 세션을 탈취하기 위한 시도 -> 세션 관리에 헛점이 없도록 구성의 기본 내용을 잘 알아야 함
(Spring과 세션이 어떤 관계를 맺고 있는지)
SessionRegistry 를 사용한다. 이 빈을 이용해 세션사용자(SessionInformation)를 모니터링
이 필터의 관심사는 오로지 동시 접속
-> SessionRegistry에 있는 SessionInformation에서 expired 된 토큰이 들어 오지 못하도록 관리하는 것
Session이란 애플리케이션 컨테이너, 즉 톰캣과 같은 Servlet Container에서 제공하는 것
-> 즉 Spring에서 세션을 제어할 순 없음
-> 톰캣이 넘겨주는 세션을 Spring은 Session Information이라는 Wrapper 객체
를 하나 만들어서 SessionRegistry
에서 관리하는 것
-> 톰캣의 Session과 SessionRegistry의 Session은 엄밀히 따지면 일치하는 것은 아님
개발자가 특정 SeesionId의 Session을 expired 마킹을 하면 더이상 안으로 들어 올 수 없게하는 것이 Concurrent Session Filter
Concurrent Session Filter
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) {
if (info.isExpired()) {
// Expired - abort processing
this.logger.debug(LogMessage
.of(() -> "Requested session ID " + request.getRequestedSessionId() + " has expired."));
doLogout(request, response);
this.sessionInformationExpiredStrategy
.onExpiredSessionDetected(new SessionInformationExpiredEvent(info, request, response));
return;
}
// Non-expired - update last request date/time
this.sessionRegistry.refreshLastRequest(info.getSessionId());
}
}
chain.doFilter(request, response);
}
세션이 Expired되었다면 sessionInformationExpiredStrategy에 따라서 expired 작업 진행
-> 어떤 세션을 Expired 시킬지는 SessionManagementFilter
에서 진행
SessionAuthenticationStrategy 에서 여러가지 세션 인증 정책을 관리하도록 설정할 수 있다
SessionAuthenticationStrategy
이라는 인터페이스 가짐
스프링에서 제공하는 Strategy Filter는 크게 2가지로 나뉨
SessionFixationProtectionStrategy
(세션 고정 정책에 관련된 필터)만약 로그인 마다 세션아이디가 바뀌지 않는다면
Ex) 어떠한 사용자가 있고 악의적인 사용자가 있다
악의적인 사용자가 정상적인 사용자의 세션 아이디에 자신의 세션아이디를 심는다 가정
정상적인 사용자가 접속을 한다면 같은 세션 아이디로 악의적인 사용자 또한 접속이 가능 함
ConcurrentSessionControlAuthenticationStrategy
(동시접속 제어를 하기 위한 필터)RegisterSessionAuthenticationStrategy 와 함께 SessionRegistry 를 참조해 작업
-> 만약 세션 갯수제한을 2개로 했는데 3개의 접속을 하게 된다면 기존에 접속되어 있는 세션의 Session Information 의 expire 마킹을 할 것인지, 아니면 새로 접속하는 세션의 expire를 마킹할 것인가
-> 이는 설정에 따라 다름
Security.java 일부
@Bean
SessionRegistry sessionRegistry(){
SessionRegistryImpl registry = new SessionRegistryImpl();
return registry;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
...
.sessionManagement(
s -> s
// .sessionFixation(sessionFixationConfigurer -> sessionFixationConfigurer.none()) // session 아이디를 고정하는 경우
.maximumSessions(1) // 동시에 한 유저당 한 세션만 허용 됨
// default인 false는 기존의 세션을 만료시키는 것
.maxSessionsPreventsLogin(false)
.expiredUrl("/session-expired")
)
Session.info
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class SessionInfo {
private String sessionId;
private Date time;
}
UserSession.java
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class UserSession {
private String username;
private List<SessionInfo> sessions;
public int getCount(){
return sessions.size();
}
}
SessionController
@Controller
public class SessionController {
// 세션 모니터링 페이지 만들기
@Autowired
private SessionRegistry sessionRegistry;
@GetMapping("/sessions")
public String sessions(Model model){
model.addAttribute("sessionList",sessionRegistry.getAllPrincipals().stream().map(p->UserSession.builder()
.username(((SpUser)p).getUsername())
.sessions(sessionRegistry.getAllSessions(p, false).stream().map(s ->
SessionInfo.builder().sessionId(s.getSessionId())
.time(s.getLastRequest()).build())
.collect(Collectors.toList()))
.build()).collect(Collectors.toList()));
return "/sessionList";
}
@PostMapping("/sessions/expire")
public String expireSession(@RequestParam String sessionId){
SessionInformation sessionInformation = sessionRegistry.getSessionInformation(sessionId);
if(!sessionInformation.isExpired()){
sessionInformation.expireNow();
}
return "redirect:/sessions";
}
@GetMapping("/session-expired")
public String sessionExpired(){
return "/sessionExpired";
}
}