前言
- SessionManagementConfigurer是SpringSecurity中比较特殊的配置类了,他会注册两个过滤器
- SessionManagementFilter:检测身份认证
- ConcurrentSessionFilter:限制并发会话过滤器
- 也是默认注册的

1. SessionManagementConfigurer
- SessionManagementConfigurer的很多方法都是对于下面的这些属性进行配置
private final SessionAuthenticationStrategy DEFAULT_SESSION_FIXATION_STRATEGY = createDefaultSessionFixationProtectionStrategy();
private SessionAuthenticationStrategy sessionFixationAuthenticationStrategy = this.DEFAULT_SESSION_FIXATION_STRATEGY;
private SessionAuthenticationStrategy sessionAuthenticationStrategy;
private SessionAuthenticationStrategy providedSessionAuthenticationStrategy;
private List<SessionAuthenticationStrategy> sessionAuthenticationStrategies = new ArrayList<>();
private InvalidSessionStrategy invalidSessionStrategy;
private String invalidSessionUrl;
private SessionInformationExpiredStrategy expiredSessionStrategy;
private String expiredUrl;
private SessionRegistry sessionRegistry;
private Integer maximumSessions;
private boolean maxSessionsPreventsLogin;
private SessionCreationPolicy sessionPolicy;
private boolean enableSessionUrlRewriting;
private AuthenticationFailureHandler sessionAuthenticationFailureHandler;
private String sessionAuthenticationErrorUrl;
1.1 SessionAuthenticationStrategy
- SessionAuthenticationStrategy:是在身份认证成功发生时 执行有关HttpSession的策略 默认会有一个防止固定会话攻击的 改变sessionId的策略
public interface SessionAuthenticationStrategy {
void onAuthentication(Authentication authentication, HttpServletRequest request, HttpServletResponse response)
throws SessionAuthenticationException;
}
- 实现类如下,接下来将一个个的讲
- ChangeSessionIdAuthenticationStrategy
- SessionFixationProtectionStrategy
- CsrfAuthenticationStrategy
- RegisterSessionAuthenticationStrategy
- ConcurrentSessionControlAuthenticationStrategy
1.1.1 ChangeSessionIdAuthenticationStrategy
- 在介绍此过滤器之前要先知道会话固定攻击的原理
- 会话固定攻击:让合法用户使用黑客预先设置的SessionID进行登录,从而让服务器不再进行生成新的SessionID,从而导致黑客设置的SessionId变成了合法桥梁
- 所以说防止会话固定攻击的原理就是在认证完成后更新SessionID
- 那现在我们先看下防止会话固定保护的基类AbstractSessionFixationProtectionStrategy的onAuthentication(...)方法
public void onAuthentication(Authentication authentication, HttpServletRequest request,
HttpServletResponse response) {
boolean hadSessionAlready = request.getSession(false) != null;
if (!hadSessionAlready && !this.alwaysCreateSession) {
return;
}
HttpSession session = request.getSession();
if (hadSessionAlready && request.isRequestedSessionIdValid()) {
String originalSessionId;
String newSessionId;
Object mutex = WebUtils.getSessionMutex(session);
synchronized (mutex) {
originalSessionId = session.getId();
session = applySessionFixation(request);
newSessionId = session.getId();
}
if (originalSessionId.equals(newSessionId)) {
this.logger.warn("Your servlet container did not change the session ID when a new session "
+ "was created. You will not be adequately protected against session-fixation attacks");
}
else {
if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Changed session id from %s", originalSessionId));
}
}
onSessionChange(originalSessionId, session, authentication);
}
}
- 很明显是通过applySessionFixation(request);方法变更了新的Session,而ChangeSessionIdAuthenticationStrategy也重写了此方法
@Override
HttpSession applySessionFixation(HttpServletRequest request) {
request.changeSessionId();
return request.getSession();
}
- 由于会话固定攻击是因为攻击者利用自己的sessionId让正常用户进行登录了,所以让用户登录后更改sessionId就好了
1.1.2 SessionFixationProtectionStrategy
- 此类和上面的ChangeSessionIdAuthenticationStrategy都是为了防止固定会话攻击的
- ChangeSessionIdAuthenticationStrategy是直接改变SessionId的,HttpSession对象没有发生变化,还是那一个地址,而SessionFixationProtectionStrategy是直接换一个新的HttpSession
final HttpSession applySessionFixation(HttpServletRequest request) {
HttpSession session = request.getSession();
String originalSessionId = session.getId();
this.logger.debug(LogMessage.of(() -> "Invalidating session with Id '" + originalSessionId + "' "
+ (this.migrateSessionAttributes ? "and" : "without") + " migrating attributes."));
Map<String, Object> attributesToMigrate = extractAttributes(session);
int maxInactiveIntervalToMigrate = session.getMaxInactiveInterval();
session.invalidate();
session = request.getSession(true);
this.logger.debug(LogMessage.format("Started new session: %s", session.getId()));
transferAttributes(attributesToMigrate, session);
if (this.migrateSessionAttributes) {
session.setMaxInactiveInterval(maxInactiveIntervalToMigrate);
}
return session;
}
1.1.3 CsrfAuthenticationStrategy
- CsrfAuthenticationStrategy:有关Csrf的HttpSession认证策略,是由于CsrfConfigurer注册的
- 是为了在认证成功后,更换新的csrfToken
@Override
public void onAuthentication(Authentication authentication, HttpServletRequest request,
HttpServletResponse response) throws SessionAuthenticationException {
boolean containsToken = this.csrfTokenRepository.loadToken(request) != null;
if (containsToken) {
this.csrfTokenRepository.saveToken(null, request, response);
CsrfToken newToken = this.csrfTokenRepository.generateToken(request);
this.csrfTokenRepository.saveToken(newToken, request, response);
request.setAttribute(CsrfToken.class.getName(), newToken);
request.setAttribute(newToken.getParameterName(), newToken);
this.logger.debug("Replaced CSRF Token");
}
}
1.1.4 RegisterSessionAuthenticationStrategy
- 此类主要是为当认证完成后将 Principal 和 SessionId 和 SessionInformation绑定起来,也是为了踢出用户做准备
@Override
public void onAuthentication(Authentication authentication, HttpServletRequest request,
HttpServletResponse response) {
this.sessionRegistry.registerNewSession(request.getSession().getId(), authentication.getPrincipal());
}
- 一个账户可以支持在不同地方认证多次,也就是说一个Principal会对应多个SessionId
1.1.5 ConcurrentSessionControlAuthenticationStrategy
- ConcurrentSessionControlAuthenticationStrategy:处理并发会话控制的策略
- 我们可以通过此类配置当某个用户登录的会话超过限制后是否阻止登录
private boolean exceptionIfMaximumExceeded = false;
- 直接看onAuthentication(...)方法
public void onAuthentication(Authentication authentication, HttpServletRequest request,
HttpServletResponse response) {
int allowedSessions = getMaximumSessionsForThisUser(authentication);
if (allowedSessions == -1) {
return;
}
List<SessionInformation> sessions = this.sessionRegistry.getAllSessions(authentication.getPrincipal(), false);
int sessionCount = sessions.size();
if (sessionCount < allowedSessions) {
return;
}
if (sessionCount == allowedSessions) {
HttpSession session = request.getSession(false);
if (session != null) {
for (SessionInformation si : sessions) {
if (si.getSessionId().equals(session.getId())) {
return;
}
}
}
}
allowableSessionsExceeded(sessions, allowedSessions, this.sessionRegistry);
}
- 然后我们再看allowableSessionsExceeded(...)方法,当会话数超过了指定限制后有两种阻止登录的操作
protected void allowableSessionsExceeded(List<SessionInformation> 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"));
}
sessions.sort(Comparator.comparing(SessionInformation::getLastRequest));
int maximumSessionsExceededBy = sessions.size() - allowableSessions + 1;
List<SessionInformation> sessionsToBeExpired = sessions.subList(0, maximumSessionsExceededBy);
for (SessionInformation session : sessionsToBeExpired) {
session.expireNow();
}
}
1.2 SessionRegistry
- 是SessionInformation的注册中心,维护了所有的关联关系
public interface SessionRegistry {
List<Object> getAllPrincipals();
List<SessionInformation> getAllSessions(Object principal, boolean includeExpiredSessions);
SessionInformation getSessionInformation(String sessionId);
void refreshLastRequest(String sessionId);
void registerNewSession(String sessionId, Object principal);
void removeSessionInformation(String sessionId);
}
- SessionRegistry只有一个实现:SessionRegistryImpl
1.2.1 SessionRegistryImpl
- 针对SpringSecurity内部维护的SessionInformation的操作 比如说用户登录后,才会创建会话session,那么通常会有一个SpringSecurity的SessionInformation的创建
- 此类的核心在于如何维护关联关系,重点就在于下面这两个Map
public class SessionRegistryImpl implements SessionRegistry, ApplicationListener<AbstractSessionEvent> {
private final ConcurrentMap<Object, Set<String>> principals;
private final Map<String, SessionInformation> sessionIds;
}
- 通过上面这两个Map我们可以通过用户对象(principal)得到此用户已经登录了哪些会话,然后就可以通过会话Id获取对应的SessionInformation了
- registerNewSession(...):注册新的映射关系
public void registerNewSession(String sessionId, Object principal) {
Assert.hasText(sessionId, "SessionId required as per interface contract");
Assert.notNull(principal, "Principal required as per interface contract");
if (getSessionInformation(sessionId) != null) {
removeSessionInformation(sessionId);
}
if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Registering session %s, for principal %s", sessionId, principal));
}
this.sessionIds.put(sessionId, new SessionInformation(principal, sessionId, new Date()));
this.principals.compute(principal, (key, sessionsUsedByPrincipal) -> {
if (sessionsUsedByPrincipal == null) {
sessionsUsedByPrincipal = new CopyOnWriteArraySet<>();
}
sessionsUsedByPrincipal.add(sessionId);
this.logger.trace(LogMessage.format("Sessions used by '%s' : %s", principal, sessionsUsedByPrincipal));
return sessionsUsedByPrincipal;
});
}
- removeSessionInformation(...):清除有关sessionId的映射关系
public void removeSessionInformation(String sessionId) {
Assert.hasText(sessionId, "SessionId required as per interface contract");
SessionInformation info = getSessionInformation(sessionId);
if (info == null) {
return;
}
if (this.logger.isTraceEnabled()) {
this.logger.debug("Removing session " + sessionId + " from set of registered sessions");
}
this.sessionIds.remove(sessionId);
this.principals.computeIfPresent(info.getPrincipal(), (key, sessionsUsedByPrincipal) -> {
this.logger.debug(
LogMessage.format("Removing session %s from principal's set of registered sessions", sessionId));
sessionsUsedByPrincipal.remove(sessionId);
if (sessionsUsedByPrincipal.isEmpty()) {
this.logger.debug(LogMessage.format("Removing principal %s from registry", info.getPrincipal()));
sessionsUsedByPrincipal = null;
}
this.logger.trace(
LogMessage.format("Sessions used by '%s' : %s", info.getPrincipal(), sessionsUsedByPrincipal));
return sessionsUsedByPrincipal;
});
}
- refreshLastRequest(..):刷新当前sessionId对应的SessionInformation的最后一次操作时间
public void refreshLastRequest(String sessionId) {
Assert.hasText(sessionId, "SessionId required as per interface contract");
SessionInformation info = getSessionInformation(sessionId);
if (info != null) {
info.refreshLastRequest();
}
}
- getAllSessions(...):获得当前用户所有的SessionInformation
public List<SessionInformation> getAllSessions(Object principal, boolean includeExpiredSessions) {
Set<String> sessionsUsedByPrincipal = this.principals.get(principal);
if (sessionsUsedByPrincipal == null) {
return Collections.emptyList();
}
List<SessionInformation> list = new ArrayList<>(sessionsUsedByPrincipal.size());
for (String sessionId : sessionsUsedByPrincipal) {
SessionInformation sessionInformation = getSessionInformation(sessionId);
if (sessionInformation == null) {
continue;
}
if (includeExpiredSessions || !sessionInformation.isExpired()) {
list.add(sessionInformation);
}
}
return list;
}
- onApplicationEvent(...):处理session无效而导致的销毁事件和sessionId发生改变的事件
public void onApplicationEvent(AbstractSessionEvent event) {
if (event instanceof SessionDestroyedEvent) {
SessionDestroyedEvent sessionDestroyedEvent = (SessionDestroyedEvent) event;
String sessionId = sessionDestroyedEvent.getId();
removeSessionInformation(sessionId);
}
else if (event instanceof SessionIdChangedEvent) {
SessionIdChangedEvent sessionIdChangedEvent = (SessionIdChangedEvent) event;
String oldSessionId = sessionIdChangedEvent.getOldSessionId();
if (this.sessionIds.containsKey(oldSessionId)) {
Object principal = this.sessionIds.get(oldSessionId).getPrincipal();
removeSessionInformation(oldSessionId);
registerNewSession(sessionIdChangedEvent.getNewSessionId(), principal);
}
}
}
- 默认我们只开启并发管理的情况下,是无法接受到AbstractSessionEvent事件的,需要往容器注册一个HttpSessionEventPublisher才可以
@Bean
HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
}
1.3 SessionCreationPolicy
- SpringSecurity的过滤器在执行过程中是否允许创建会话的策略,在SecurityContextPersistenceFilter中会被用到
public enum SessionCreationPolicy {
ALWAYS,
NEVER,
IF_REQUIRED,
STATELESS
}
1.4 init(...)
public void init(H http) {
SecurityContextRepository securityContextRepository = http.getSharedObject(SecurityContextRepository.class);
boolean stateless = isStateless();
if (securityContextRepository == null) {
if (stateless) {
http.setSharedObject(SecurityContextRepository.class, new NullSecurityContextRepository());
}
else {
HttpSessionSecurityContextRepository httpSecurityRepository = new HttpSessionSecurityContextRepository();
httpSecurityRepository.setDisableUrlRewriting(!this.enableSessionUrlRewriting);
httpSecurityRepository.setAllowSessionCreation(isAllowSessionCreation());
AuthenticationTrustResolver trustResolver = http.getSharedObject(AuthenticationTrustResolver.class);
if (trustResolver != null) {
httpSecurityRepository.setTrustResolver(trustResolver);
}
http.setSharedObject(SecurityContextRepository.class, httpSecurityRepository);
}
}
RequestCache requestCache = http.getSharedObject(RequestCache.class);
if (requestCache == null) {
if (stateless) {
http.setSharedObject(RequestCache.class, new NullRequestCache());
}
}
http.setSharedObject(SessionAuthenticationStrategy.class, getSessionAuthenticationStrategy(http));
http.setSharedObject(InvalidSessionStrategy.class, getInvalidSessionStrategy());
}
- getSessionAuthenticationStrategy(...):获得HttpSession认证策略
private SessionAuthenticationStrategy getSessionAuthenticationStrategy(H http) {
if (this.sessionAuthenticationStrategy != null) {
return this.sessionAuthenticationStrategy;
}
List<SessionAuthenticationStrategy> delegateStrategies = this.sessionAuthenticationStrategies;
SessionAuthenticationStrategy defaultSessionAuthenticationStrategy;
if (this.providedSessionAuthenticationStrategy == null) {
defaultSessionAuthenticationStrategy = postProcess(this.sessionFixationAuthenticationStrategy);
}
else {
defaultSessionAuthenticationStrategy = this.providedSessionAuthenticationStrategy;
}
if (isConcurrentSessionControlEnabled()) {
SessionRegistry sessionRegistry = getSessionRegistry(http);
ConcurrentSessionControlAuthenticationStrategy concurrentSessionControlStrategy = new ConcurrentSessionControlAuthenticationStrategy(
sessionRegistry);
concurrentSessionControlStrategy.setMaximumSessions(this.maximumSessions);
concurrentSessionControlStrategy.setExceptionIfMaximumExceeded(this.maxSessionsPreventsLogin);
concurrentSessionControlStrategy = postProcess(concurrentSessionControlStrategy);
RegisterSessionAuthenticationStrategy registerSessionStrategy = new RegisterSessionAuthenticationStrategy(
sessionRegistry);
registerSessionStrategy = postProcess(registerSessionStrategy);
delegateStrategies.addAll(Arrays.asList(concurrentSessionControlStrategy,
defaultSessionAuthenticationStrategy, registerSessionStrategy));
}
else {
delegateStrategies.add(defaultSessionAuthenticationStrategy);
}
this.sessionAuthenticationStrategy = postProcess(
new CompositeSessionAuthenticationStrategy(delegateStrategies));
return this.sessionAuthenticationStrategy;
}
1.5 configure(...)
public void configure(H http) {
SecurityContextRepository securityContextRepository = http.getSharedObject(SecurityContextRepository.class);
SessionManagementFilter sessionManagementFilter = new SessionManagementFilter(securityContextRepository,
getSessionAuthenticationStrategy(http));
if (this.sessionAuthenticationErrorUrl != null) {
sessionManagementFilter.setAuthenticationFailureHandler(
new SimpleUrlAuthenticationFailureHandler(this.sessionAuthenticationErrorUrl));
}
InvalidSessionStrategy strategy = getInvalidSessionStrategy();
if (strategy != null) {
sessionManagementFilter.setInvalidSessionStrategy(strategy);
}
AuthenticationFailureHandler failureHandler = getSessionAuthenticationFailureHandler();
if (failureHandler != null) {
sessionManagementFilter.setAuthenticationFailureHandler(failureHandler);
}
AuthenticationTrustResolver trustResolver = http.getSharedObject(AuthenticationTrustResolver.class);
if (trustResolver != null) {
sessionManagementFilter.setTrustResolver(trustResolver);
}
sessionManagementFilter = postProcess(sessionManagementFilter);
http.addFilter(sessionManagementFilter);
if (isConcurrentSessionControlEnabled()) {
ConcurrentSessionFilter concurrentSessionFilter = createConcurrencyFilter(http);
concurrentSessionFilter = postProcess(concurrentSessionFilter);
http.addFilter(concurrentSessionFilter);
}
}
- createConcurrencyFilter(...):创建一个有关并发会话限制的过滤器
private ConcurrentSessionFilter createConcurrencyFilter(H http) {
SessionInformationExpiredStrategy expireStrategy = getExpiredSessionStrategy();
SessionRegistry sessionRegistry = getSessionRegistry(http);
ConcurrentSessionFilter concurrentSessionFilter = (expireStrategy != null)
? new ConcurrentSessionFilter(sessionRegistry, expireStrategy)
: new ConcurrentSessionFilter(sessionRegistry);
LogoutConfigurer<H> logoutConfigurer = http.getConfigurer(LogoutConfigurer.class);
if (logoutConfigurer != null) {
List<LogoutHandler> logoutHandlers = logoutConfigurer.getLogoutHandlers();
if (!CollectionUtils.isEmpty(logoutHandlers)) {
concurrentSessionFilter.setLogoutHandlers(logoutHandlers);
}
}
return concurrentSessionFilter;
}
2. ConcurrentSessionFilter
- 此过滤器是限制并发会话的过滤器,作用如下
- 作用1:对于已经过期的SessionInformation进行登出操作,然后执行过期策略
- 作用2:更新操作时间,这是因为如果并发数达到限制,可以会根据最后操作时间来踢出用户
- 直接看核心的doFilter(...)方法
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()) {
this.logger.debug(LogMessage
.of(() -> "Requested session ID " + request.getRequestedSessionId() + " has expired."));
doLogout(request, response);
this.sessionInformationExpiredStrategy
.onExpiredSessionDetected(new SessionInformationExpiredEvent(info, request, response));
return;
}
this.sessionRegistry.refreshLastRequest(info.getSessionId());
}
}
chain.doFilter(request, response);
}
- 这里重点就在于如何知道这个SessionInformation过期了呢?
- 我们可以回到ConcurrentSessionControlAuthenticationStrategy的allowableSessionsExceeded(...)方法中

- 这里是发生在用户认证完成后,超过了最大会话数的时候会将某些SessionInformation直接设置为已过期,所以说知道过滤器的顺序是十分重要,具体顺序见FilterComparator类
3. SessionManagementFilter
- 检测从执行 SecurityContextPersistenceFilter 到执行本过滤器之间是否就已经通过身份验证 如果已经通过,那么就执行HttpSession认证策略和检查HttpSession是否已经无效
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
if (request.getAttribute(FILTER_APPLIED) != null) {
chain.doFilter(request, response);
return;
}
request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
if (!this.securityContextRepository.containsContext(request)) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && !this.trustResolver.isAnonymous(authentication)) {
try {
this.sessionAuthenticationStrategy.onAuthentication(authentication, request, response);
}
catch (SessionAuthenticationException ex) {
this.logger.debug("SessionAuthenticationStrategy rejected the authentication object", ex);
SecurityContextHolder.clearContext();
this.failureHandler.onAuthenticationFailure(request, response, ex);
return;
}
this.securityContextRepository.saveContext(SecurityContextHolder.getContext(), request, response);
}
else {
if (request.getRequestedSessionId() != null && !request.isRequestedSessionIdValid()) {
if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Request requested invalid session id %s",
request.getRequestedSessionId()));
}
if (this.invalidSessionStrategy != null) {
this.invalidSessionStrategy.onInvalidSessionDetected(request, response);
return;
}
}
}
}
chain.doFilter(request, response);
}
- 这段代码的核心在于要执行HttpSession认证策略,也就说第19行代码
- 我们想象一下用表单认证的情况也就说使用UsernamePasswordAuthenticationFilter进行认证
- 当认证完成后我们就会执行下面的方法然后就会将认证对象保存在线程中(默认的实现)

- 然后再借助RequestCache机制,将当前请求重定向到原请求上

- 然后我们可以发现在UsernamePasswordAuthenticationFilter中不会执行下一个过滤器

- 然后在SecurityContextPersistenceFilter.doFilter(...)中的finally代码块中就会将SecurityContext保存在Session中(默认实现)

- 所以说在表单认证方式下可能存在 认证对象在HttpSession中不存在,而在线程中存在吗?,肯定是不可能是
- 但是其他认证方式呢?比如说是通过记住我令牌认证的就会有这种情况
- 因为在RememberMeAuthenticationFilter中不会执行HttpSession认证策略,所以说需要在SessionManagementFilter中去执行