一.简介
在SpringSecurity中实现会话并发控制,只需要配置一个会话数量就可以了,先介绍下如何配置会话并发控制,然后再。介绍下SpringSecurity 如何实现会话并发控制。
二.创建项目
如何创建一个SpringSecurity项目,前面文章已经有说明了,这里就不重复写了。
三.代码实现
3.1设置只有一个会话
SecurityConfig 类,代码如下:
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
()
.anyRequest().authenticated()
.and()
.formLogin()
.permitAll()
.and()
.sessionManagement()
.maximumSessions(1);
return ();
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
登陆一个客户端
登陆第二个客户端
刷新第一个客户端
这时候发现已经被挤掉了。
目前默认策略是:后来的会把前面的给挤掉,现在我们通过配置,禁止第二个客户端登陆
3.2禁止第二个客户端登陆
SecurityConfig 类,代码如下:
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
()
.anyRequest().authenticated()
.and()
.formLogin()
.permitAll()
.and()
.sessionManagement()
.maximumSessions(1)
.maxSessionsPreventsLogin(true);
return ();
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
登陆第一个客户端
登陆第二个客户端
四.实现原理分析
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链中
- ! && 添加 DisableEncodeUrlFilter
- && 添加 ForceEagerSessionCreationFilter
4.2CompositeSessionAuthenticationStrategy
CompositeSessionAuthenticationStrategy是一个代理策略,它里面会包含很多SessionAuthenticationStrategy,主要有ConcurrentSessionControlAuthenticationStrategy和RegisterSessionAuthenticationStrategy。
4.3RegisterSessionAuthenticationStrategy
处理并发登录人数的数量,代码如下:
public void onAuthentication(Authentication authentication, HttpServletRequest request,
HttpServletResponse response) {
(().getId(), ());
}
- 1
- 2
- 3
- 4
这里直接调用方法,代码如下:
public void registerNewSession(String sessionId, Object principal) {
(sessionId, "SessionId required as per interface contract");
(principal, "Principal required as per interface contract");
if (getSessionInformation(sessionId) != null) {
removeSessionInformation(sessionId);
}
(sessionId, new SessionInformation(principal, sessionId, new Date()));
(principal, (key, sessionsUsedByPrincipal) -> {
if (sessionsUsedByPrincipal == null) {
sessionsUsedByPrincipal = new CopyOnWriteArraySet<>();
}
(sessionId);
(("Sessions used by '%s' : %s", principal, sessionsUsedByPrincipal));
return sessionsUsedByPrincipal;
});
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 根据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 = ((), false);
int sessionCount = ();
if (sessionCount < allowedSessions) {
// They haven't got too many login sessions running at present
return;
}
if (sessionCount == allowedSessions) {
HttpSession session = (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 (().equals(())) {
return;
}
}
}
// If the session is null, a new one will be created by the parent class,
// exceeding the allowed number
}
allowableSessionsExceeded(sessions, allowedSessions, );
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 获取当前用户允许同时在线的数量,如果 == -1(没有限制)则跳过并发校验
- 获取当前用户的所有在线session数量,如果小于限制数量则返回
- 如果等于限制数量,则判断当前的sessionId是否已经在集合中,如果在,则返回
- 否则走allowableSessionsExceeded 校验
allowableSessionsExceeded方法代码如下:
protected void allowableSessionsExceeded(List<SessionInformation> sessions, int allowableSessions,
SessionRegistry registry) throws SessionAuthenticationException {
if ( || (sessions == null)) {
throw new SessionAuthenticationException(
("",
new Object[] { allowableSessions }, "Maximum sessions of {0} for this principal exceeded"));
}
// Determine least recently used sessions, and mark them for invalidation
((SessionInformation::getLastRequest));
int maximumSessionsExceededBy = () - allowableSessions + 1;
List<SessionInformation> sessionsToBeExpired = (0, maximumSessionsExceededBy);
for (SessionInformation session : sessionsToBeExpired) {
();
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 如果配置了 maxSessionsPreventsLogin,则直接抛出异常,禁止新用户登录,否则往下走
- 将当前用户的所有session按照最后访问时间排序
- 获取最大允许同时在线的数量,然后在集合中 top n,其余的全部设置过期
- expireNow();
= true;
4.5SessionManagementFilter
代码如下:
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
if (!(request)) {
Authentication authentication = ().getAuthentication();
if (authentication != null && !(authentication)) {
// The user has been authenticated during the current request, so call the
// session strategy
try {
(authentication, request, response);
}
catch (SessionAuthenticationException ex) {
();
(request, response, ex);
return;
} ((), request, response);
}
else {
if (() != null && !()) {
if ( != null) {
(request, response);
return;
}
}
}
}
(request, response);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 如果securityContextRepository 有Context信息
- 如果抛出异常,则进行异常处理,并清楚context信息
获取authentication
如果authentication 不为空,则调用
如果为空,则调用invalidSessionStrategy的onInvalidSessionDetected方法
4.6ConcurrentSessionFilter
ConcurrentSessionFilter类,代码如下:
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpSession session = (false);
if (session != null) {
SessionInformation info = (());
if (info != null) {
if (()) {
// Expired - abort processing
(LogMessage
.of(() -> "Requested session ID " + () + " has expired."));
doLogout(request, response);
.onExpiredSessionDetected(new SessionInformationExpiredEvent(info, request, response));
return;
}
// Non-expired - update last request date/time
(());
}
}
(request, response);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
4.7AbstractAuthenticationProcessingFilter
这个过滤器也会调用,进行session维护,代码如下:
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
Authentication authenticationResult = attemptAuthentication(request, response);
(authenticationResult, request, response);
}
- 1
- 2
- 3
- 4
- 5
- 6
整体流程图,截图如下: