Session [Session] management and defense in Spring Security and concurrency control of sessions

       As we all know, HTTP itself has no content about the current user status, that is, there is no correlation between two HTTP requests. During the interaction between the user and the server, the site cannot determine which user is visiting, and therefore cannot Provide corresponding personalized services. The birth of Session is to solve this problem, providing a solution for stateless HTTP request to maintain user state - the server and the user agree that each HTTP request carries an ID information [representing the current user information], so as to realize different There is an association between requests. When a user visits for the first time, a sessionId will be automatically generated for the user, and then recorded with a cookie as a carrier. The content in the cookie will be brought when the user visits in the middle of the session cycle, so the system can identify which user it is.

1. Session fixation attack

        Although cookies are very useful, some users will turn off cookies for the purpose of security or protection of personal privacy, as shown in the settings in Google Chrome above. In this case, you cannot implement Session based on Cookie, which makes the user experience not very good. So for this, some websites provide URL rewriting to achieve a similar experience. But in this case, there will be a problem: URL rewriting will directly splice the SessionId on the URL, that is, for example, as shown below.

http://www.test.com/test;jsessionid=ByOK3vjFD75aPnrF7C2HmdnV6QZcEbzWoWiBYEnLerjQ99zWpBng!-145788764

        This method is easy to be used by hackers. Hackers only need to visit the site once, paste the SessionId generated by the system to this URL, and then give the URL to the user. As long as the user logs in through this URL within the session validity period, the sessionId It will be bound to the user's identity, and the hacker can easily enjoy the same session state without the need for a user name and password. This is a typical session fixation attack.

        The defense against session fixation attacks is actually very simple, that is, the user refreshes the SessionId after logging in, so that the original SessionId becomes useless. In Spring Security, this method is enabled for us by default, so we don't need special configuration. But we can also configure it manually. There are four strategies for defending against session fixation attacks in Spring Security:

newSession: create a new session after login

migrateSession: Create a new session after login, and copy the data in the old session.

changeSessionId: Do not create a new session, but use the session fixation protection provided by the Servlet container.

none: do not make any changes, use the old session after login

        In fact, these four strategies correspond to three objects of the following four methods: SessionFixationProtectionStrategy, ChangeSessionIdAuthenticationStrategy, NullAuthenticatedSessionStrategy.

         The configuration method is also very simple. In the Spring Security 5.2.8 version I downloaded, changeSessionId is specified by default.

         In addition to changing the value of SessionId, it can also be defended by setting a session expiration policy. By default, the session expiration time is 30 minutes. It is also possible to specify an invalidation policy.

2. Session concurrency control

        The concept of session concurrency control is reflected in the video software we often use, such as Tencent Video and iQiyi. If I purchase a membership for my account, I can share this account with my friends and family. Of course, this kind of login must not be unlimited, just like for Tencent Video and iQiyi, if the number of devices that log in to the account is not limited, it will definitely be a loss, and it is not conducive to the security of your own information. Therefore, the number of devices that can log in at the same time will be limited. Once this limit is exceeded, the previous account will be kicked out. This is the so-called concurrency control.

//session相关的控制
.sessionManagement()
  //指定最大的session并发数量 
  .maximumSessions(1) 

        The setting of the concurrent number of sessions is very simple, just use maximumSessions. Without additional configuration, a relogin session kicks out the old session. Before introducing session concurrency, you need to understand an object called SessionRegistry-manage the user's session state, which can also be called the user session information table. The reason why it can be called a user session information table is because it maintains two ConcurrentHashMap objects principals and sessionIds, which store the principal and session information SessionInformation respectively. Principal said in the previous article that it contains the information of the subject, and SessionInformation actually includes the subject information, sessionId, whether it expires, and the time of the last request.

       The main function of SessionInformation is to record Session information in concurrency control in Spring Security. Session has three states in Security: Active (active), Expired (expired) and Destroyed (invalid). To invalidate a Session, you can invalidate it through the invalidate method of the Session itself, or you can destroy it through Servlet container management. Session expiration is largely due to the fact that the user's maximum number of sessions has reached the limit. At this time, the session must be expired, and the expired session will be quickly deleted through the filter.

        With the understanding of SessionInformation, let's look at SessionRegistryImpl, a class that implements SessionRegistry, and the specific operation of the session information table is also in this implementation class.

public class SessionRegistryImpl implements SessionRegistry, ApplicationListener<SessionDestroyedEvent> {
    protected final Log logger = LogFactory.getLog(SessionRegistryImpl.class);
	// 用户及其对应Session,一个用户可能有多个session
    private final ConcurrentMap<Object, Set<String>> principals;
	// SessionId及其对应的SessionInformation
    private final Map<String, SessionInformation> sessionIds;
    public SessionRegistryImpl() {
        this.principals = new ConcurrentHashMap();
        this.sessionIds = new ConcurrentHashMap();
    }
    public SessionRegistryImpl(ConcurrentMap<Object, Set<String>> principals, Map<String, SessionInformation> sessionIds) {
        this.principals = principals;
        this.sessionIds = sessionIds;
    }
	// 获取当前的所有主体信息
    public List<Object> getAllPrincipals() {
        return new ArrayList(this.principals.keySet());
    }
	// 获取主体对应的会话信息,可包含过期或者不过期的SessionInformation
    public List<SessionInformation> getAllSessions(Object principal, boolean includeExpiredSessions) {
        // 获取当前主体的所有会话ID
        Set<String> sessionsUsedByPrincipal = (Set)this.principals.get(principal);
        if (sessionsUsedByPrincipal == null) {
            return Collections.emptyList();
        } else {
            List<SessionInformation> list = new ArrayList(sessionsUsedByPrincipal.size());
            Iterator var5 = sessionsUsedByPrincipal.iterator();
            while(true) {
                SessionInformation sessionInformation;
                
                do {
                 // 获取对应ID的SessionInformation,若没有则继续获取,循环结束则直接返回List
                    do {
                        if (!var5.hasNext()) {
                            return list;
                        }
                        String sessionId = (String)var5.next();
                        sessionInformation = this.getSessionInformation(sessionId);
                    } while(sessionInformation == null);
                // 查询是否过期的且当前SessionInformation是否过期
                } while(!includeExpiredSessions && sessionInformation.isExpired());
                // 满足条件则添加至List
                list.add(sessionInformation);
            }
        }
    }
	// 根据ID获取对应的SessionInformation
    public SessionInformation getSessionInformation(String sessionId) {
        Assert.hasText(sessionId, "SessionId required as per interface contract");
        return (SessionInformation)this.sessionIds.get(sessionId);
    }
	// 实现onApplicationEvent接口,表明处理SessionDestoryedEvent事件
    public void onApplicationEvent(SessionDestroyedEvent event) {
        String sessionId = event.getId();
		// 移除对应sessionId的相关数据
        this.removeSessionInformation(sessionId);
    }
	// 刷新最近操作日期
    public void refreshLastRequest(String sessionId) {
        Assert.hasText(sessionId, "SessionId required as per interface contract");
        SessionInformation info = this.getSessionInformation(sessionId);
        if (info != null) {
            info.refreshLastRequest();
        }
    }
	// 新增会话信息
	// SessionManagementConfigure默认会将RegisterSessionAuthenticationStrategy添加
	// 到一个组合式的SessionAuthenticationStrategy中,
	// 并由AbstractAuthenticationProcessingFilter在成功调用时,触发该动作。
    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 (this.getSessionInformation(sessionId) != null) {
            this.removeSessionInformation(sessionId);
        }
        if (this.logger.isDebugEnabled()) {
            this.logger.debug("Registering session " + sessionId + ", for principal " + principal);
        }
		// 会话信息
        this.sessionIds.put(sessionId, new SessionInformation(principal, sessionId, new Date()));
		// 判断用户是否存在
        this.principals.compute(principal, (key, sessionsUsedByPrincipal) -> {
            if (sessionsUsedByPrincipal == null) {
                sessionsUsedByPrincipal = new CopyOnWriteArraySet();
            }
			// 若用户存在,将当前sessionId添加到对应的集合中。
			// 用Set即可实现去重
            ((Set)sessionsUsedByPrincipal).add(sessionId);
            if (this.logger.isTraceEnabled()) {
                this.logger.trace("Sessions used by '" + principal + "' : " + sessionsUsedByPrincipal);
            }
            return (Set)sessionsUsedByPrincipal;
        });
    }
	// 删除会话信息
    public void removeSessionInformation(String sessionId) {
        Assert.hasText(sessionId, "SessionId required as per interface contract");
        SessionInformation info = this.getSessionInformation(sessionId);
        if (info != null) {
            if (this.logger.isTraceEnabled()) {
                this.logger.debug("Removing session " + sessionId + " from set of registered sessions");
            }
			// 以String类型的Key删除对应的sessionId及其Information
            this.sessionIds.remove(sessionId);
            this.principals.computeIfPresent(info.getPrincipal(), (key, sessionsUsedByPrincipal) -> {
                if (this.logger.isDebugEnabled()) {
                    this.logger.debug("Removing session " + sessionId + " from principal's set of registered sessions");
                }
				// 将用户会话记录中的,对应用户的对应session删除
                sessionsUsedByPrincipal.remove(sessionId);
				// 如果获取成功,则清理对应的内容
                if (sessionsUsedByPrincipal.isEmpty()) {
                    if (this.logger.isDebugEnabled()) {
                        this.logger.debug("Removing principal " + info.getPrincipal() + " from registry");
                    }
                    sessionsUsedByPrincipal = null;
                }
                if (this.logger.isTraceEnabled()) {
                    this.logger.trace("Sessions used by '" + info.getPrincipal() + "' : " + sessionsUsedByPrincipal);
                }
                return sessionsUsedByPrincipal;
            });
        }
    }
}

        For SessionRegistryImpl, we have a few extra points to pay attention to. If you don't pay attention, it is easy to hit a wall in the subsequent use process.

        First, the Principals in the object use user information as the Key . In HashMap, the two methods of hashCode and equals must be overridden by using the object as the key. Therefore, these two methods must be overridden when implementing UserDetails. If there is no overridden, the same user will be calculated every time he logs in and logs out. Keys are different, so each login adds a user to Principals, and logout never effectively removes it. In this case, not only the effect of session concurrency control cannot be achieved, but also memory leaks will be caused.

        The second point, we noticed that SessionRegistryImpl actually implements the interface ApplicationListener. The way to monitor Session-related events in Servlet is to implement the HttpSessionListener interface and register the listener in the system. In Spring Security, the HttpSessionEventPublisher interface is implemented in the HttpSessionEventPublisher class, and converted into Spring's event mechanism, so there is also the SessionDestroyedEvent event, that is, the session destruction event. Therefore, in order to implement event monitoring, HttpSessionEventPublisher must be registered in the IOC container, so that Java time can be converted into Spring events [ as long as the session management function is used, HttpSessionEventPublisher should be configured ].

 

        With the above understanding, let's analyze the concurrency control strategy at this time. After reading the following concurrency control strategy, you will actually find that there is only an operation to control the expiration of the session state when the number of sessions is exceeded. There was no registration and deletion of sessions at that time. This is what the second point above said. Session creation and deletion events in Security are implemented through Spring's event mechanism. We can also see that Creation and Destoryed have corresponding events in the same package of SessionRegistryImpl. Through this Only two events are registered and destroyed.

public class ConcurrentSessionControlAuthenticationStrategy implements MessageSourceAware, SessionAuthenticationStrategy {
    protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
    private final SessionRegistry sessionRegistry;
	// 如果超出最大会话数是否阻止新会话的建立
    private boolean exceptionIfMaximumExceeded = false;
	// 最大的会话数
    private int maximumSessions = 1;
    public ConcurrentSessionControlAuthenticationStrategy(SessionRegistry sessionRegistry) {
        Assert.notNull(sessionRegistry, "The sessionRegistry cannot be null");
        this.sessionRegistry = sessionRegistry;
    }
    public void onAuthentication(Authentication authentication, HttpServletRequest request, HttpServletResponse response) {
		// 获取当前用户的所有有效的会话信息
        List<SessionInformation> sessions = this.sessionRegistry.getAllSessions(authentication.getPrincipal(), false);
		
        int sessionCount = sessions.size();
        int allowedSessions = this.getMaximumSessionsForThisUser(authentication);
		// 判断当前用户的会话数量是否超过最大值
        if (sessionCount >= allowedSessions) {
			// 如果最大会话数量为-1,则默认不限制会话数量
            if (allowedSessions != -1) {
				// 当已存在的会话数量等于最大会话数时
                if (sessionCount == allowedSessions) {
					// 判断当前会话是否已经在用户对应的会话列表中
                    HttpSession session = request.getSession(false);
                    if (session != null) {
                        Iterator var8 = sessions.iterator();
                        while(var8.hasNext()) {
                            SessionInformation si = (SessionInformation)var8.next();
							// 当前验证的会话并不是新的会话,则不做任何的处理
                            if (si.getSessionId().equals(session.getId())) {
                                return;
                            }
                        }
                    }
                }
				// 进行策略判断
                this.allowableSessionsExceeded(sessions, allowedSessions, this.sessionRegistry);
            }
        }
    }
    ......
    protected void allowableSessionsExceeded(List<SessionInformation> sessions, int allowableSessions, SessionRegistry registry) throws SessionAuthenticationException {
        // 当用户达到最大会话数时,是否阻止新会话的建立
		if (!this.exceptionIfMaximumExceeded && sessions != null) {
			// 按照建立会话时间先后升序排序,
            sessions.sort(Comparator.comparing(SessionInformation::getLastRequest));
			// 取待过期的会话 
            int maximumSessionsExceededBy = sessions.size() - allowableSessions + 1;
            List<SessionInformation> sessionsToBeExpired = sessions.subList(0, maximumSessionsExceededBy);
            Iterator var6 = sessionsToBeExpired.iterator();
            while(var6.hasNext()) {
				// 新会话建立,使最早的会话过期
                SessionInformation session = (SessionInformation)var6.next();
                session.expireNow();
            }

        } else {
			// 提示会话已超过数量
            throw new SessionAuthenticationException(this.messages.getMessage("ConcurrentSessionControlAuthenticationStrategy.exceededAllowed", new Object[]{allowableSessions}, "Maximum sessions of {0} for this principal exceeded"));
        }
    }
    ......
}

        Through the above analysis, we can also understand why the official website's explanation of SessionInformation proposed three states of Session at the beginning: Active, Expired, and Destoryed. Because in session concurrency control, the three states are controlled by three modules.

Guess you like

Origin blog.csdn.net/qq_35363507/article/details/121901049