一.简介
在SpringSecurity中实现会话并发控制,只需要配置一个会话数量就可以了,先介绍下如何配置会话并发控制,然后再。介绍下SpringSecurity 如何实现会话并发控制。
二.创建项目
如何创建一个SpringSecurity项目,前面文章已经有说明了,这里就不重复写了。
三.代码实现
3.1设置只有一个会话
SecurityConfig 类,代码如下:
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.permitAll()
.and()
.sessionManagement()
.maximumSessions(1);
return http.build();
}
}
登陆一个客户端
登陆第二个客户端
刷新第一个客户端
这时候发现已经被挤掉了。
目前默认策略是:后来的会把前面的给挤掉,现在我们通过配置,禁止第二个客户端登陆
3.2禁止第二个客户端登陆
SecurityConfig 类,代码如下:
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.permitAll()
.and()
.sessionManagement()
.maximumSessions(1)
.maxSessionsPreventsLogin(true);
return http.build();
}
}
登陆第一个客户端
登陆第二个客户端
四.实现原理分析
session默认的过滤器是SessionManagementConfigurer
4.1SessionManagementConfigurer
点击.sessionManagement()进去,找到SessionManagementConfigurer,点进去看下主要是看init和configure方法。
4.1.1 init()方法
- 创建SecurityContextRepository
- 初始化 SessionAuthenticationStrategy,并添加到容器
ConcurrentSessionControlAuthenticationStrategy
defaultSessionAuthenticationStrategy
RegisterSessionAuthenticationStrategy
setMaximumSessions
setExceptionIfMaximumExceeded = maxSessionsPreventsLogin
CompositeSessionAuthenticationStrategy
InvalidSessionStrategy
4.1.2 configure()方法
- 初始化 SessionManagementFilter
- 添加sessionManagementFilter 到http链中
- isConcurrentSessionControlEnabled &&添加ConcurrentSessionFilter 到http链中
- !this.enableSessionUrlRewriting && 添加 DisableEncodeUrlFilter
- SessionCreationPolicy.ALWAYS && 添加 ForceEagerSessionCreationFilter
4.2CompositeSessionAuthenticationStrategy
CompositeSessionAuthenticationStrategy是一个代理策略,它里面会包含很多SessionAuthenticationStrategy,主要有ConcurrentSessionControlAuthenticationStrategy和RegisterSessionAuthenticationStrategy。
4.3RegisterSessionAuthenticationStrategy
处理并发登录人数的数量,代码如下:
public void onAuthentication(Authentication authentication, HttpServletRequest request,
HttpServletResponse response) {
this.sessionRegistry.registerNewSession(request.getSession().getId(), authentication.getPrincipal());
}
这里直接调用this.sessionRegistry.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);
}
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;
});
}
- 根据sessionId查找session,如果有,则移除
- 创建新的SessionInformation,维护到sessionIds中
- 维护sessionId到principals中
4.4ConcurrentSessionControlAuthenticationStrategy
onAuthentication方法代码如下:
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(没有限制)则跳过并发校验
- 获取当前用户的所有在线session数量,如果小于限制数量则返回
- 如果等于限制数量,则判断当前的sessionId是否已经在集合中,如果在,则返回
- 否则走allowableSessionsExceeded 校验
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"));
}
// 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();
}
}
- 如果配置了 maxSessionsPreventsLogin,则直接抛出异常,禁止新用户登录,否则往下走
- 将当前用户的所有session按照最后访问时间排序
- 获取最大允许同时在线的数量,然后在集合中 top n,其余的全部设置过期
- expireNow();
this.expired = true;
4.5SessionManagementFilter
代码如下:
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
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) {
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.invalidSessionStrategy != null) {
this.invalidSessionStrategy.onInvalidSessionDetected(request, response);
return;
}
}
}
}
chain.doFilter(request, response);
}
- 如果securityContextRepository 有Context信息
- 如果抛出异常,则进行异常处理,并清楚context信息
获取authentication
如果authentication 不为空,则调用sessionAuthenticationStrategy.onAuthentication
如果为空,则调用invalidSessionStrategy的onInvalidSessionDetected方法
4.6ConcurrentSessionFilter
ConcurrentSessionFilter类,代码如下:
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);
}
4.7AbstractAuthenticationProcessingFilter
这个过滤器也会调用sessionStrategy.onAuthentication,进行session维护,代码如下:
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
Authentication authenticationResult = attemptAuthentication(request, response);
this.sessionStrategy.onAuthentication(authenticationResult, request, response);
}
整体流程图,截图如下: