spring-security 是怎么实现同一个用户登录次数限制的(包含源码分析)

先上一张流程图

主要配置源码:
package com.example.demo.sms;

import com.example.demo.core.AbstractSecurityConfigurerAdapter;
import com.example.demo.core.MySimpleRedirectSessionInformationExpiredStrategy;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.session.CompositeSessionAuthenticationStrategy;
import org.springframework.security.web.authentication.session.ConcurrentSessionControlAuthenticationStrategy;
import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
import org.springframework.security.web.session.ConcurrentSessionFilter;

import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;


@Configuration
public class SmsLoginConfig extends AbstractSecurityConfigurerAdapter {

    @Resource
    private SessionRegistry sessionRegistry;


    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private  AuthenticationSuccessHandler authenticationSuccessHandler;

    @Autowired
    private AuthenticationFailureHandler authenticationFailureHandler;


    @Override
    public void configure(HttpSecurity http) throws Exception {

       SmsAuthenticationProcessingFilter processingFilter = new SmsAuthenticationProcessingFilter("/sms/login");
        processingFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler);
        processingFilter.setAuthenticationFailureHandler(authenticationFailureHandler);
        processingFilter.setAuthenticationManager(authenticationManager);


        //注册新session策略
        RegisterSessionAuthenticationStrategy registerSessionAuthenticationStrategy = new RegisterSessionAuthenticationStrategy(sessionRegistry);
        registerSessionAuthenticationStrategy = postProcess(registerSessionAuthenticationStrategy);

        //session 管理策略,
        ConcurrentSessionControlAuthenticationStrategy concurrentSessionControlStrategy = new ConcurrentSessionControlAuthenticationStrategy(
                sessionRegistry);


        // -------------一个用户登录一次设置  start--------------------------
        //设置 限制的同一个用户登录的次数
        concurrentSessionControlStrategy.setMaximumSessions(1);
        //作用是当session超过次数限制时,是当前用户无法登陆,还是之前登录过期的策略设置,默认是之前过期
        concurrentSessionControlStrategy.setExceptionIfMaximumExceeded(false);
        List<SessionAuthenticationStrategy> delegateStrategies = new ArrayList<>();
        delegateStrategies.addAll(Arrays.asList(concurrentSessionControlStrategy,registerSessionAuthenticationStrategy));
        //compositeSessionAuthenticationStrategy是存放SessionAuthenticationStrategy的集合
        CompositeSessionAuthenticationStrategy compositeSessionAuthenticationStrategy = new CompositeSessionAuthenticationStrategy(delegateStrategies);

        //设置 session 策略
        processingFilter.setSessionAuthenticationStrategy(compositeSessionAuthenticationStrategy);

        //ConcurrentSessionFilter的作用是判断session是否过期,如果过期就执行过期方法(是requst中的session无效,清除凭证)
        http.addFilterAt(new ConcurrentSessionFilter(sessionRegistry,new SimpleRedirectSessionInformationExpiredStrategy()), ConcurrentSessionFilter.class);

        // -------------一个用户登录一次设置  end--------------------------

        http.addFilterBefore(processingFilter, UsernamePasswordAuthenticationFilter.class);

        //MyAuthenticationProvider 真正认证
        http.authenticationProvider(new SmsAuthenticationProvider());
    }

   @Bean
    public SessionRegistry sessionRegistry() {
        return new SessionRegistryImpl();
    }


}

 

 

介绍下各个类的作用:

ConcurrentSessionFilter:是判断session是否过期,如果过期就执行过期方法
SessionRegistryImpl:存放认证通过的session消息
concurrentSessionControlStrategy:判断是否超过设置的session限制
registerSessionAuthenticationStrategy:注册新session
CompositeSessionAuthenticationStrategy:存放上两个策略
关键源码如下,如果有不懂可以提问。
1.设置策略。可以通过自定义实现类AbstractAuthenticationProcessingFilter#setSessionAuthenticationStrategy方法设置策略

   public void setSessionAuthenticationStrategy(
         SessionAuthenticationStrategy sessionStrategy) {
      this.sessionStrategy = sessionStrategy;
   }
   
   
入口方法(认证成功会掉调用):
AbstractAuthenticationProcessingFilter#sessionStrategy.onAuthentication(authResult, request, response);

# CompositeSessionAuthenticationStrategy 是个可以存放多个sessionStrategy的封装类
CompositeSessionAuthenticationStrategy存放两个策略concurrentSessionControlStrategy,registerSessionAuthenticationStrategy会循环调用,顺序不能变
   public void onAuthentication(Authentication authentication,
         HttpServletRequest request, HttpServletResponse response)
               throws SessionAuthenticationException {
      #遍历每个sessionStrategy执行onAuthentication方法,
      #我的配置中设置了两个策略concurrentSessionControlStrategy,registerSessionAuthenticationStrategy,
      #并且构造方法都用了同一个SessionRegistryImpl
      for (SessionAuthenticationStrategy delegate : this.delegateStrategies) {
         if (this.logger.isDebugEnabled()) {
            this.logger.debug("Delegating to " + delegate);
         }
         delegate.onAuthentication(authentication, request, response);
      }
   }



   两个策略concurrentSessionControlStrategy,registerSessionAuthenticationStrategy的关键源码如下:
   //ConcurrentSessionControlAuthenticationStrategy作用:判断当前登录用户名所存在的session是否在允许的最大登录数内,如果是直接返回,否就会执行
   //registerSessionAuthenticationStrategy 注册新session


   concurrentSessionControlStrategy类执行如下,
   public void onAuthentication(Authentication authentication,
             HttpServletRequest request, HttpServletResponse response) {

          final List<SessionInformation> sessions = sessionRegistry.getAllSessions(
                authentication.getPrincipal(), false);

          int sessionCount = sessions.size();
          #可以通过 concurrentSessionControlStrategy.setMaximumSessions(1)改变同一用户登录次数
          int allowedSessions = getMaximumSessionsForThisUser(authentication);

          if (sessionCount < allowedSessions) {
             // They haven't got too many login sessions running at present
             return;
          }

          if (allowedSessions == -1) {
             // We permit unlimited logins
             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
                //第一次登录会执行如下方法,因为是同一个sessionId
                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
          }

            //如果超过设置,就会执行sessions是之前该用户登录的session集合,allowableSessions是运行的个数,registry是SessionRegistryImpl,第二次登录会调用
          allowableSessionsExceeded(List<SessionInformation> sessions,
                     int allowableSessions, SessionRegistry registry)

       }




          protected void allowableSessionsExceeded(List<SessionInformation> sessions,
                 int allowableSessions, SessionRegistry registry)
                 throws SessionAuthenticationException {
                 }
              //属性exceptionIfMaximumExceeded是决定如果登录超过限制,是限制当前登录用户异常,还是将之前登录用户强制退出策略(默认是false),
              if (exceptionIfMaximumExceeded || (sessions == null)) {
                 throw new SessionAuthenticationException(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就设置为无效
                 session.expireNow();
              }
           }


   RegisterSessionAuthenticationStrategy作用是增加新sessionInfo
      public void onAuthentication(Authentication authentication,
             HttpServletRequest request, HttpServletResponse response) {
             
          sessionRegistry.registerNewSession(request.getSession().getId(),
                authentication.getPrincipal());
       }

       SessionRegistryImpl
       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 (logger.isDebugEnabled()) {
                 logger.debug("Registering session " + sessionId + ", for principal "
                       + principal);
              }

                //放置sessionId-SessionInformation(principal, sessionId, new Date())
              sessionIds.put(sessionId,
                    new SessionInformation(principal, sessionId, new Date()));

                //放置principal--sessionIds
              principals.compute(principal, (key, sessionsUsedByPrincipal) -> {
                 if (sessionsUsedByPrincipal == null) {
                    sessionsUsedByPrincipal = new CopyOnWriteArraySet<>();
                 }
                 sessionsUsedByPrincipal.add(sessionId);

                 if (logger.isTraceEnabled()) {
                    logger.trace("Sessions used by '" + principal + "' : "
                          + sessionsUsedByPrincipal);
                 }
                 return sessionsUsedByPrincipal;
              });
           }

    SessionRegistryImpl 部分源码
    public class SessionRegistryImpl implements SessionRegistry,
          ApplicationListener<SessionDestroyedEvent> {

       // ~ Instance fields
       // ================================================================================================

       protected final Log logger = LogFactory.getLog(SessionRegistryImpl.class);

        #存放的key是用户名 ,value 是 sessionId集合
       private final ConcurrentMap<Object, Set<String>> principals;

       private final Map<String, SessionInformation> sessionIds;


       public SessionRegistryImpl() {
          this.principals = new ConcurrentHashMap<>();
          this.sessionIds = new ConcurrentHashMap<>();
       }




        //ConcurrentSessionFilter源码:判断是否过期,如果过期执行过期方法,如果没有过期刷新请求时间
       public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
             throws IOException, ServletException {
          HttpServletRequest request = (HttpServletRequest) req;
          HttpServletResponse response = (HttpServletResponse) res;

          HttpSession session = request.getSession(false);

          if (session != null) {
          //根据sessionId去获取SessionInformation
             SessionInformation info = sessionRegistry.getSessionInformation(session
                   .getId());

             if (info != null) {
             //判断是否过期,只有登录次数限制才设置了过期,所以一定是其他设备上登录导致的过期
                if (info.isExpired()) {
                   // Expired - abort processing
                   if (logger.isDebugEnabled()) {
                      logger.debug("Requested session ID "
                            + request.getRequestedSessionId() + " has expired.");
                   }
                   //设置了tomcat的session失效,清除凭证
                   doLogout(request, response);

                        //回调过期处理方法,可以自定义,该类构造方法就可以自定义设置
                   this.sessionInformationExpiredStrategy.onExpiredSessionDetected(new SessionInformationExpiredEvent(info, request, response));
                   return;
                }
                else {
                   // Non-expired - update last request date/time
                   sessionRegistry.refreshLastRequest(info.getSessionId());
                }
             }
          }

          chain.doFilter(request, response);
       }
  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值