- 서버는 기본적으로 사용자를 보면서 판단할 수 없습니다. 서버는 로그인을 통해 요청을 보낸 사용자를 구분합니다. 하지만 모든 요청에 아이디/패스워드를 물어볼 수는 없습니다. 그래서 토큰을 발급하고, 세션에는 토큰을 저장해 놓고 세션이 유지되는 동안, 혹은 remember-me 토큰이 있다면 해당 토큰이 살아있는 동안 로그인 없이 해당 토큰만으로 사용자를 인증하고 요청을 처리해 줍니다. 그래서 악의적으로 정보를 취하고자 하는 사람들(해커)은 세션을 탈취하기 위한 시도를 합니다. 따라서 세션 관리에 헛점이 없도록 구성의 기본 내용을 잘 알아야 합니다.
SecurityContextPersistenceFilter

- SecurityContextRepository 인터페이스 구현체를 통해 사용자의 SecurityContext를 가져오거나 갱신함
- 인증 관련 필터 중 가장 최 상단에 위치 — 이미 인증된 사용자는 다시 로그인할 필요가 없음
- SecurityContext가 존재하지 않는다면, empty SecurityContext를 생성함
SecurityContextRepository
인터페이스 기본 구현은 Session을 이용하는HttpSessionSecurityContextRepository
클래스
private SecurityContextRepository repo; private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { // ensure that filter is only applied once per request if (request.getAttribute(FILTER_APPLIED) != null) { chain.doFilter(request, response); return; } request.setAttribute(FILTER_APPLIED, Boolean.TRUE); if (this.forceEagerSessionCreation) { HttpSession session = request.getSession(); if (this.logger.isDebugEnabled() && session.isNew()) { this.logger.debug(LogMessage.format("Created session %s eagerly", session.getId())); } } HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response); SecurityContext contextBeforeChainExecution = this.repo.loadContext(holder); try { SecurityContextHolder.setContext(contextBeforeChainExecution); if (contextBeforeChainExecution.getAuthentication() == null) { logger.debug("Set SecurityContextHolder to empty SecurityContext"); } else { if (this.logger.isDebugEnabled()) { this.logger .debug(LogMessage.format("Set SecurityContextHolder to %s", contextBeforeChainExecution)); } } chain.doFilter(holder.getRequest(), holder.getResponse()); } finally { SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext(); // Crucial removal of SecurityContextHolder contents before anything else. SecurityContextHolder.clearContext(); this.repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse()); request.removeAttribute(FILTER_APPLIED); this.logger.debug("Cleared SecurityContextHolder to complete request"); } }
- Request 클래스의 requestedSessionId라는 필드에 cookie의 JSESSIONID값을 매핑해서 값을 넣어줌
public class Request implements HttpServletRequest{ public void setRequestedSessionId(String id) { this.requestedSessionId = id; } }
- repo.loadContext를 통해 request로 들어온 sessionId를 보고 Session에서 SecurityContext를 불러오고
@Override public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) { HttpServletRequest request = requestResponseHolder.getRequest(); HttpServletResponse response = requestResponseHolder.getResponse(); HttpSession httpSession = request.getSession(false); SecurityContext context = readSecurityContextFromSession(httpSession); if (context == null) { context = generateNewContext(); if (this.logger.isTraceEnabled()) { this.logger.trace(LogMessage.format("Created %s", context)); } } SaveToSessionResponseWrapper wrappedResponse = new SaveToSessionResponseWrapper(response, request, httpSession != null, context); requestResponseHolder.setResponse(wrappedResponse); requestResponseHolder.setRequest(new SaveToSessionRequestWrapper(request, wrappedResponse)); return context; }
- repo.saveContext를 통해 세션에 SecurityContext를 저장하고 response가 반환됨
SessionManagementFilter
- 세션 고정 보호 (session-fixation protection)
- [참고] Understanding Session Fixation Attacks
- session-fixation attack — 세션 하이재킹 기법중 하나로 정상 사용자의 세션을 탈취하여 인증을 우회하는 기법
- 인증 전에 발급 받은 세션 ID가 인증 후에도 동일하게 사용되면 발생할 수 있음
- 즉, 인증 전에 사용자가 가지고 있던 세션이 인증 후에는 사용되지 않도록 하면 해당 공격에 효과적으로 대응할 수 있음
- Spring Security에서는 4가지 설정 가능한 옵션을 제공함( sessionFixation() )
- none — 아무것도 하지 않음 (세션을 그대로 유지함)
- newSession — 새로운 세션을 만들고, 기존 데이터는 복제하지 않음
- migrateSession — 새로운 세션을 만들고, 데이터를 모두 복제함
- changeSession — 새로운 세션을 만들지 않지만, session-fixation 공격을 방어함 (단, servlet 3.1 이상에서만 지원)


- 유효하지 않은 세션 감지 시 지정된 URL로 리다이렉트 시킴 (invalidSessionUrl( ) )
- 세션 생성 전략 설정(sessionCreationPolicy( ) )
- IF_REQUIRED — 필요시 생성함 (기본값)
- NEVER — Spring Security에서는 세션을 생성하지 않지만, 세션이 존재하면 사용함
- STATELESS — 세션을 완전히 사용하지 않음 (JWT 인증이 사용되는 REST API 서비스에 적합)
- ALWAYS — 항상 세션을 사용함
- 동일 사용자의 중복 로그인 감지 및 처리
- maximumSessions — 동일 사용자의 최대 동시 세션 갯수
- maxSessionsPreventsLogin — 최대 갯수를 초과하게 될 경우 인증 시도를 차단할지 여부 (기본값 false)
@Override protected void configure(HttpSecurity http) throws Exception { http /** * 세션 관련 설정 */ .sessionManagement() .sessionFixation().changeSessionId() .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) .invalidSessionUrl("/") .maximumSessions(1) .maxSessionsPreventsLogin(false) .and() .and() ; }
- AbstractAuthenticationProcessingFilter 객체는 SessionManagementFilter와 동일한 세션 고정 보호, 최대 로그인 세션 제어를 수행함
- 위 두 개의 필터는 SessionAuthenticationStrategy 객체를 공유함
- AbstractAuthenticationProcessingFilter 구현을 보면, 인증 처리가 완료 된 후 SessionAuthenticationStrategy 객체를 통해 필요한 처리를 수행하고 있음
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { // 생략 ... try { Authentication authenticationResult = attemptAuthentication(request, response); if (authenticationResult == null) { // return immediately as subclass has indicated that it hasn't completed return; } this.sessionStrategy.onAuthentication(authenticationResult, request, response); // Authentication success if (this.continueChainBeforeSuccessfulAuthentication) { chain.doFilter(request, response); } successfulAuthentication(request, response, chain, authenticationResult); } // 생략 ... }