org.springframework.security.web.authentication.session.ConcurrentSessionControlAuthenticationStrategy Maven / Gradle / Ivy
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.authentication.session;
import java.util.Comparator;
import java.util.List;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import org.springframework.context.MessageSource;
import org.springframework.context.MessageSourceAware;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.SpringSecurityMessageSource;
import org.springframework.security.core.session.SessionInformation;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.session.ConcurrentSessionFilter;
import org.springframework.security.web.session.SessionManagementFilter;
import org.springframework.util.Assert;
/**
* Strategy which handles concurrent session-control.
*
*
* When invoked following an authentication, it will check whether the user in question
* should be allowed to proceed, by comparing the number of sessions they already have
* active with the configured maximumSessions value. The {@link SessionRegistry}
* is used as the source of data on authenticated users and session data.
*
*
* If a user has reached the maximum number of permitted sessions, the behaviour depends
* on the exceptionIfMaxExceeded property. The default behaviour is to expire any
* sessions that exceed the maximum number of permitted sessions, starting with the least
* recently used sessions. The expired sessions will be invalidated by the
* {@link ConcurrentSessionFilter} if accessed again. If exceptionIfMaxExceeded
* is set to true, however, the user will be prevented from starting a new
* authenticated session.
*
*
* This strategy can be injected into both the {@link SessionManagementFilter} and
* instances of {@link AbstractAuthenticationProcessingFilter} (typically
* {@link UsernamePasswordAuthenticationFilter}), but is typically combined with
* {@link RegisterSessionAuthenticationStrategy} using
* {@link CompositeSessionAuthenticationStrategy}.
*
*
* @author Luke Taylor
* @author Rob Winch
* @since 3.2
* @see CompositeSessionAuthenticationStrategy
*/
public class ConcurrentSessionControlAuthenticationStrategy
implements MessageSourceAware, SessionAuthenticationStrategy {
protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
private final SessionRegistry sessionRegistry;
private boolean exceptionIfMaximumExceeded = false;
private int maximumSessions = 1;
/**
* @param sessionRegistry the session registry which should be updated when the
* authenticated session is changed.
*/
public ConcurrentSessionControlAuthenticationStrategy(SessionRegistry sessionRegistry) {
Assert.notNull(sessionRegistry, "The sessionRegistry cannot be null");
this.sessionRegistry = sessionRegistry;
}
/**
* In addition to the steps from the superclass, the sessionRegistry will be updated
* with the new session information.
*/
@Override
public void onAuthentication(Authentication authentication, HttpServletRequest request,
HttpServletResponse response) {
int allowedSessions = getMaximumSessionsForThisUser(authentication);
if (allowedSessions == -1) {
// We permit unlimited logins
return;
}
List sessions = this.sessionRegistry.getAllSessions(authentication.getPrincipal(), false);
int sessionCount = sessions.size();
if (sessionCount < allowedSessions) {
// They haven't got too many login sessions running at present
return;
}
if (sessionCount == allowedSessions) {
HttpSession session = request.getSession(false);
if (session != null) {
// Only permit it though if this request is associated with one of the
// already registered sessions
for (SessionInformation si : sessions) {
if (si.getSessionId().equals(session.getId())) {
return;
}
}
}
// If the session is null, a new one will be created by the parent class,
// exceeding the allowed number
}
allowableSessionsExceeded(sessions, allowedSessions, this.sessionRegistry);
}
/**
* Method intended for use by subclasses to override the maximum number of sessions
* that are permitted for a particular authentication. The default implementation
* simply returns the maximumSessions
value for the bean.
* @param authentication to determine the maximum sessions for
* @return either -1 meaning unlimited, or a positive integer to limit (never zero)
*/
protected int getMaximumSessionsForThisUser(Authentication authentication) {
return this.maximumSessions;
}
/**
* Allows subclasses to customise behaviour when too many sessions are detected.
* @param sessions either null
or all unexpired sessions associated with
* the principal
* @param allowableSessions the number of concurrent sessions the user is allowed to
* have
* @param registry an instance of the SessionRegistry
for subclass use
*
*/
protected void allowableSessionsExceeded(List sessions, int allowableSessions,
SessionRegistry registry) throws SessionAuthenticationException {
if (this.exceptionIfMaximumExceeded || (sessions == null)) {
throw new SessionAuthenticationException(
this.messages.getMessage("ConcurrentSessionControlAuthenticationStrategy.exceededAllowed",
new Object[] { allowableSessions }, "Maximum sessions of {0} for this principal exceeded"));
}
// Determine least recently used sessions, and mark them for invalidation
sessions.sort(Comparator.comparing(SessionInformation::getLastRequest));
int maximumSessionsExceededBy = sessions.size() - allowableSessions + 1;
List sessionsToBeExpired = sessions.subList(0, maximumSessionsExceededBy);
for (SessionInformation session : sessionsToBeExpired) {
session.expireNow();
}
}
/**
* Sets the exceptionIfMaximumExceeded property, which determines whether the
* user should be prevented from opening more sessions than allowed. If set to
* true, a SessionAuthenticationException will be raised which means
* the user authenticating will be prevented from authenticating. if set to
* false, the user that has already authenticated will be forcibly logged
* out.
* @param exceptionIfMaximumExceeded defaults to false.
*/
public void setExceptionIfMaximumExceeded(boolean exceptionIfMaximumExceeded) {
this.exceptionIfMaximumExceeded = exceptionIfMaximumExceeded;
}
/**
* Sets the maxSessions property. The default value is 1. Use -1 for
* unlimited sessions.
* @param maximumSessions the maximimum number of permitted sessions a user can have
* open simultaneously.
*/
public void setMaximumSessions(int maximumSessions) {
Assert.isTrue(maximumSessions != 0,
"MaximumLogins must be either -1 to allow unlimited logins, or a positive integer to specify a maximum");
this.maximumSessions = maximumSessions;
}
/**
* Sets the {@link MessageSource} used for reporting errors back to the user when the
* user has exceeded the maximum number of authentications.
*/
@Override
public void setMessageSource(MessageSource messageSource) {
Assert.notNull(messageSource, "messageSource cannot be null");
this.messages = new MessageSourceAccessor(messageSource);
}
}