SessionManagementFilter检测用户自请求开始以来是否已通过身份验证,如果已通过,则调用SessionAuthenticationStrategy以执行任何与会话相关的活动,例如激活会话固定保护机制或检查多个并发登录。配置如下:
@Configuration
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.formLogin()
.permitAll()
.and()
.sessionManagement()
.sessionFixation()
.newSession()
.maximumSessions(1);
}
}
sessionFixation是配置会话固定保护策略的。maximumSessions配置session最大并发数。
源码解析
SessionManagementFilter
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)) {
// The user has been authenticated during the current request, so call the
// session strategy
try {
this.sessionAuthenticationStrategy.onAuthentication(authentication, request, response);
}
catch (SessionAuthenticationException ex) {
// The session strategy can reject the authentication
this.logger.debug("SessionAuthenticationStrategy rejected the authentication object", ex);
SecurityContextHolder.clearContext();
this.failureHandler.onAuthenticationFailure(request, response, ex);
return;
}
// Eagerly save the security context to make it available for any possible
// re-entrant requests which may occur before the current request
// completes. SEC-1396.
this.securityContextRepository.saveContext(SecurityContextHolder.getContext(), request, response);
}
else {
// No security context or authentication present. Check for a session
// timeout
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);
}
1、通过request.getAttribute(FILTER_APPLIED)判断请求是否已经通过身份验证,如果通过则放行否则进行下一步。
2、通过this.securityContextRepository.containsContext(request)判断当前请求是否包含security context(默认是判断session是否已有SPRING_SECURITY_CONTEXT属性,在保存security context时会设置SPRING_SECURITY_CONTEXT属性)。如果已经包含则放行。否则进行下一步。
3、从SecurityContextHolder中获取Authentication(身份鉴权对象),Authentication存在且不是匿名用户调用sessionAuthenticationStrategy.onAuthentication执行任何与会话相关的活动,例如激活会话固定保护机制或检查多个并发登录。failureHandler.onAuthenticationFailure处理异常。
4、如果Authentication不存在或是匿名用户则invalidSessionStrategy.onInvalidSessionDetected处理session过期。
并发session由ConcurrentSessionControlAuthenticationStrategy处理。
ConcurrentSessionControlAuthenticationStrategy#onAuthentication(Authentication authentication, HttpServletRequest request,HttpServletResponse response)
public void onAuthentication(Authentication authentication, HttpServletRequest request,
HttpServletResponse response) {
int allowedSessions = getMaximumSessionsForThisUser(authentication);
if (allowedSessions == -1) {
// We permit unlimited logins
return;
}
List<SessionInformation> 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);
}
1、getMaximumSessionsForThisUser通过鉴权用户获取最大session并发数,就是上面配置类设置的maximumSessions。如果不限制session并发数,即maximumSessions等于-1.返回。
2、sessionRegistry.getAllSessions通过Principal(用户对象)获取所有的session。实际是ConcurrentMap保存了每个登录用户的sessionId。登录用户作为key,如果自定义登录用户对象就必须要实现equals和hashCode方法。
3、如果用户的登录session数等于最大并发数,则判断在登录的session里面是否存在本次登录的session,如果存在则返回。否则调用allowableSessionsExceeded处理并发session问题。
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"));
}
// Determine least recently used sessions, and mark them for invalidation
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();
}
}
将session按照lastRequest排序后将多出来的session调用expireNow设置为过期。
固定session保护
会话固定攻击参考什么是会话固定攻击?Spring Boot 中要如何防御会话固定攻击? 。固定session保护策略由sessionFixation()配置,有四个选项:
- newSession():每个请求创建新会话,但不应保留原始HttpSession中的会话属性。
- changeSessionId():session不变,使用HttpServlet Request.changeSessionId()防止会话固定攻击,默认实现
- migrateSession():每个请求创建新会话,并将原来的session属性复制到新的session中。
- none():不开启会话固定保护。