shiro realizes user kickout function

Keywords: Session Shiro Apache Redis

It seems that most of the Java Web community still knows Kaitao's name. Many people's introductory tutorials such as spring mvc, spring, shiro started with Kaitao's blog.

Shiro's single-user login function is also referenced from Kaitao's blog code. Chapter 18 Concurrent Login Number Control --"Learn Shiro from Me"

Then the realization of our system is as follows:

Version 1:

package com.air.tqb.shiro.filter;
 
import com.air.tqb.common.AppConstant;
import com.air.tqb.model.TbOrganization;
import com.air.tqb.utils.WxbStatic;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.SessionException;
import org.apache.shiro.session.mgt.DefaultSessionKey;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.session.mgt.eis.CachingSessionDAO;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.AccessControlFilter;
import org.apache.shiro.web.util.WebUtils;
import org.joda.time.DateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.ListOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
 
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.Serializable;
import java.util.Collections;
 
 
/**
 * Created by qixiaobo on 2017/3/10.
 */
public class KickoutSessionControlFilter extends AccessControlFilter {
    private static final String SESSION_KEY_KICKOUT = "kickout";
    private static final String SESSION_KEY_KICKOUT_TIME = "kickout_time";
    private static final String SESSION_KEY_KICKOUT_IP = "kickout_ip";
    private static final String REDIS_KEY_PREFIX = CachingSessionDAO.ACTIVE_SESSION_CACHE_NAME + ":";
    private Logger logger = LoggerFactory.getLogger(KickoutSessionControlFilter.class);
    private String kickoutUrl; //Address after kicking out
    private boolean kickoutAfter; //Users who logged in before or after kicking out default users who logged in before kicking out
    private int maxSession; //Maximum number of sessions for the same account by default 1
    private SessionManager sessionManager;
    @Autowired
    @Qualifier(value = "stringRedisTemplate")
    private StringRedisTemplate template;
    @Autowired
    private RedisScript<Boolean> addSessionAndExpireList;
    @Value("#{T(java.lang.String).valueOf(${session.validation.interval}/1000)}")
    private String sessionExpire;
    @Value("${shiro.kickout}")
    private boolean enable;
 
 
    public void setKickoutUrl(String kickoutUrl) {
        this.kickoutUrl = kickoutUrl;
    }
 
    public void setKickoutAfter(boolean kickoutAfter) {
        this.kickoutAfter = kickoutAfter;
    }
 
    public void setMaxSession(int maxSession) {
        this.maxSession = maxSession;
    }
 
    public void setSessionManager(SessionManager sessionManager) {
        this.sessionManager = sessionManager;
    }
 
 
    /**
     * If access is allowed, return true to indicate permission
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
        return !enable;
    }
 
    /**
     * Represents whether to handle access rejection on its own, if returning true indicates that it does not process and continues to execute the interceptor chain, returning false indicates that it has processed (such as redirecting to another page).
     */
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        Subject subject = getSubject(request, response);
        Session session;
        String username;
        try {
            username = (String) subject.getPrincipal();
            session = subject.getSession();
            if (!subject.isAuthenticated() && !subject.isRemembered()) {
                //If there is no login, proceed directly to the following process
                return true;
            }
        } catch (SessionException ex) {
            return true;
        }
        if (username == null || session == null) {
            return true;
        }
 
 
        String sessionId = (String) session.getId();
        ListOperations<String, String> lop = template.opsForList();
        final String redisListKey = getRedisKey(username);
        //Normally, maxSession 1 does not judge size.
        try {
            if (session.getAttribute(SESSION_KEY_KICKOUT) == null) {
                template.execute(addSessionAndExpireList, Collections.singletonList(redisListKey), sessionId, sessionExpire);
            }
            //If the number of sessionId in the queue exceeds the maximum number of sessions, start kicking.
            while (lop.size(redisListKey) > maxSession) {
                Serializable kickoutSessionId;
                if (kickoutAfter) { //If you kick out the latter
                    kickoutSessionId = lop.rightPop(redisListKey);
                } else { //Otherwise, kick out the former.
                    kickoutSessionId = lop.leftPop(redisListKey);
                }
                Session kickoutSession;
                try {
                    kickoutSession = sessionManager.getSession(new DefaultSessionKey(kickoutSessionId));
                } catch (SessionException exception) {
                    logger.warn(exception.getMessage(), exception);
                    kickoutSession = null;
                }
                if (kickoutSession != null) {
                    //Setting the kickout attribute of the session means kickout
                    kickoutSession.setAttribute(SESSION_KEY_KICKOUT, true);
                    kickoutSession.setAttribute(SESSION_KEY_KICKOUT_TIME, new DateTime().toString(AppConstant.DEFAULT_DATE_FORMAT_PATTERN));
                    kickoutSession.setAttribute(SESSION_KEY_KICKOUT_IP, WxbStatic.getRemoteIp((HttpServletRequest) request));
                }
            }
        } catch (Exception ex) {
            logger.error(ex.getMessage(), ex);
        }
 
        //If kicked out, exit directly and redirect to the address after kicked out
        if (session.getAttribute(SESSION_KEY_KICKOUT) != null) {
            //The conversation was kicked out
            try {
                TbOrganization organization = (TbOrganization) session.getAttribute("organization");
                logger.warn("{},{} kickout by {} at {}!", organization == null ? "" : organization.getPkId(), subject.getPrincipal(), session.getAttribute(SESSION_KEY_KICKOUT_IP), session.getAttribute(SESSION_KEY_KICKOUT_TIME));
            } catch (SessionException e) {
                logger.warn(e.getMessage(), e);
            } catch (Exception e) {
                logger.error(e.getMessage(), e);
            } finally {
                try {
                    subject.logout();
                } catch (Exception ex) {
                    logger.error(ex.getMessage(), ex);
                }
            }
            WebUtils.issueRedirect(request, response, kickoutUrl);
            return false;
        }
        return true;
    }
 
    public StringRedisTemplate getTemplate() {
        return template;
    }
 
    public void setTemplate(StringRedisTemplate template) {
        this.template = template;
    }
 
    private String getRedisKey(String key) {
        return REDIS_KEY_PREFIX + key;
    }
}
--
-- Created by IntelliJ IDEA.
-- User: qixiaobo
-- Date: 2017/3/12
-- Time: 16:42
-- Add to session id to list,When session Do not add if it exists or add to list At the same time update expiration time, use lua Script redis Operation Assurance Atomicity
-- keys[1]Corresponding redis Of list Of key
-- args[1]Stored as needed sessionId
-- args[2]Corresponding to this list Need expiration time, unit seconds
-- Return whether to save or not redis
local list_key = KEYS[1];
local session_id = ARGV[1];
local list = redis.call('LRANGE', list_key, 0, -1);
local exist_key = false;
for x = 1, #list do
    if list[x] == session_id then
        exist_key = true;
        break;
    end;
end;
if not exist_key then
    redis.call('RPUSH', list_key, session_id);
    redis.call('EXPIRE', list_key, ARGV[2]);
end;
return exist_key == false;
<bean id="kickoutSessionControlFilter" class="com.air.tqb.shiro.filter.KickoutSessionControlFilter">
    <property name="sessionManager" ref="sessionManager"/>
    <property name="kickoutAfter" value="false"/>
    <property name="maxSession" value="1"/>
    <property name="kickoutUrl" value="${wxb.url}?kickout"/>
</bean>
<! - Configure shiroFi lt er - >
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
    <property name="securityManager" ref="securityManager"/>
    <property name="loginUrl" value="${wxb.url}"/>
    <property name="successUrl" value="/kzf6/page/index/index.jsp" />
    <property name="unauthorizedUrl" value="/kzf6/page/error/403.jsp" />
    <property name="filters">
        <map>
            <entry key="kickout" value-ref="kickoutSessionControlFilter"/>
        </map>
    </property>
    <! - Configuration of interception rules - >
    <! - The first half of the equation represents the url or resource path - >.
    <! -- The second half of the equation defines access rights - >.
    <! - anon: Anonymous privileges, accessible directly without authentication and authorization - >
    <! - authc: Authentication is required to access - >
    <! - authc, perms [maintain:*]: Authentication is required and specific permission authorization is required to access - > Authentication
    <! - authc, roles [admin]: Authentication is required and specific role authorization is required for access - > Authentication
    <! - Access permissions of resources are matched from top to bottom of the defined rules - >.
    <property name="filterChainDefinitions">
        <value>
           
            /mlogin/login.json = anon
            <! - In addition to the URLs and resources defined above, they need to be authenticated before they can be accessed - >
            /** = kickout,authc
        </value>
    </property>
</bean>

From the above code, basically through redis script to solve the lock problem, while the next time the kicked user goes online, the log can be typed out.

But there is a problem.

The function of the code is not completely split.

The above code accomplishes two things

  1. After successful login, check if there is a user already logged in. If the kick-out strategy is executed, otherwise the normal login will occur. (The implicit idea is that the user must log in without all checks.)
  2. Whether the check was kicked out when the user visited, if kicked out, jumped to the kicked screen

So the simple optimization ideas are as follows:

 

Version 2

package com.air.tqb.shiro.filter;
 
import com.air.tqb.model.TbOrganization;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.SessionException;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.AccessControlFilter;
import org.apache.shiro.web.util.WebUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
 
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
 
 
/**
 * Created by qixiaobo on 2017/3/10.
 */
public class KickoutSessionControlFilter extends AccessControlFilter {
    private static final String SESSION_KEY_KICKOUT = "kickout";
    private static final String SESSION_KEY_KICKOUT_TIME = "kickout_time";
    private static final String SESSION_KEY_KICKOUT_IP = "kickout_ip";
    private Logger logger = LoggerFactory.getLogger(KickoutSessionControlFilter.class);
    private String kickoutUrl; //Address after kicking out
    @Value("${shiro.kickout}")
    private boolean enable;
 
 
    public void setKickoutUrl(String kickoutUrl) {
        this.kickoutUrl = kickoutUrl;
    }
 
 
    /**
     * If access is allowed, return true to indicate permission
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
        return !enable;
    }
 
    /**
     * Represents whether to handle access rejection on its own, if returning true indicates that it does not process and continues to execute the interceptor chain, returning false indicates that it has processed (such as redirecting to another page).
     */
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        Subject subject = getSubject(request, response);
        Session session;
        String username;
        try {
            username = (String) subject.getPrincipal();
            session = subject.getSession();
            if (!subject.isAuthenticated() && !subject.isRemembered()) {
                //If there is no login, proceed directly to the following process
                return true;
            }
        } catch (SessionException ex) {
            return true;
        }
        if (username == null || session == null) {
            return true;
        }
        //If kicked out, exit directly and redirect to the address after kicked out
        if (session.getAttribute(SESSION_KEY_KICKOUT) != null) {
            //The conversation was kicked out
            try {
                TbOrganization organization = (TbOrganization) session.getAttribute("organization");
                logger.warn("{},{} kickout by {} at {}!", organization == null ? "" : organization.getPkId(), subject.getPrincipal(), session.getAttribute(SESSION_KEY_KICKOUT_IP), session.getAttribute(SESSION_KEY_KICKOUT_TIME));
            } catch (SessionException e) {
                logger.warn(e.getMessage(), e);
            } catch (Exception e) {
                logger.error(e.getMessage(), e);
            } finally {
                try {
                    subject.logout();
                } catch (Exception ex) {
                    logger.error(ex.getMessage(), ex);
                }
            }
            WebUtils.issueRedirect(request, response, kickoutUrl);
            return false;
        }
        return true;
    }
 
 
}
package com.air.tqb.shiro.listener;
 
import com.air.tqb.common.AppConstant;
import com.air.tqb.utils.WxbStatic;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationListener;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.SessionException;
import org.apache.shiro.session.mgt.DefaultSessionKey;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.session.mgt.eis.CachingSessionDAO;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.util.WebUtils;
import org.joda.time.DateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.ListOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
 
import javax.servlet.http.HttpServletRequest;
import java.io.Serializable;
import java.util.Collections;
 
/**
 * Created by qixiaobo on 2017/5/22.
 */
public class KickOutSessionListener implements AuthenticationListener {
 
    private static final String SESSION_KEY_KICKOUT = "kickout";
    private static final String SESSION_KEY_KICKOUT_TIME = "kickout_time";
    private static final String SESSION_KEY_KICKOUT_IP = "kickout_ip";
    private static final String REDIS_KEY_PREFIX = CachingSessionDAO.ACTIVE_SESSION_CACHE_NAME + ":";
    private Logger logger = LoggerFactory.getLogger(KickOutSessionListener.class);
    private boolean kickoutAfter; //Users who logged in before or after kicking out default users who logged in before kicking out
    private int maxSession; //Maximum number of sessions for the same account by default 1
    private SessionManager sessionManager;
    @Autowired
    @Qualifier(value = "stringRedisTemplate")
    private StringRedisTemplate template;
    @Autowired
    private RedisScript<Boolean> addSessionAndExpireList;
    @Value("#{T(java.lang.String).valueOf(${session.validation.interval}/1000)}")
    private String sessionExpire;
    @Value("${shiro.kickout}")
    private boolean enable;
 
    @Override
    public void onSuccess(AuthenticationToken token, AuthenticationInfo info) {
        if (enable) {
            Subject subject = SecurityUtils.getSubject();
            HttpServletRequest request = WebUtils.getHttpRequest(subject);
            Session session;
            final String username = token.getPrincipal().toString();
            try {
                session = subject.getSession();
            } catch (SessionException ex) {
                logger.warn(ex.getMessage(), ex);
                return;
            }
            if (session == null) {
                return;
            }
 
            String sessionId = (String) session.getId();
            ListOperations<String, String> lop = template.opsForList();
            final String redisListKey = getRedisKey(username);
            //Normally, maxSession 1 does not judge size.
            try {
                if (session.getAttribute(SESSION_KEY_KICKOUT) == null) {
                    template.execute(addSessionAndExpireList, Collections.singletonList(redisListKey), sessionId, sessionExpire);
                }
                //If the number of sessionId in the queue exceeds the maximum number of sessions, start kicking.
                while (lop.size(redisListKey) > maxSession) {
                    Serializable kickoutSessionId;
                    if (kickoutAfter) { //If you kick out the latter
                        kickoutSessionId = lop.rightPop(redisListKey);
                    } else { //Otherwise, kick out the former.
                        kickoutSessionId = lop.leftPop(redisListKey);
                    }
                    Session kickoutSession;
                    try {
                        kickoutSession = sessionManager.getSession(new DefaultSessionKey(kickoutSessionId));
                    } catch (SessionException exception) {
                        logger.warn(exception.getMessage(), exception);
                        kickoutSession = null;
                    }
                    if (kickoutSession != null) {
                        //Setting the kickout attribute of the session means kickout
                        kickoutSession.setAttribute(SESSION_KEY_KICKOUT, true);
                        kickoutSession.setAttribute(SESSION_KEY_KICKOUT_TIME, new DateTime().toString(AppConstant.DEFAULT_DATE_FORMAT_PATTERN));
                        kickoutSession.setAttribute(SESSION_KEY_KICKOUT_IP, WxbStatic.getRemoteIp(request));
                    }
                }
            } catch (Exception ex) {
                logger.error(ex.getMessage(), ex);
            }
 
 
        }
    }
 
    @Override
    public void onFailure(AuthenticationToken token, AuthenticationException ae) {
 
    }
 
    @Override
    public void onLogout(PrincipalCollection principals) {
 
    }
 
 
    public void setKickoutAfter(boolean kickoutAfter) {
        this.kickoutAfter = kickoutAfter;
    }
 
    public void setMaxSession(int maxSession) {
        this.maxSession = maxSession;
    }
 
    public void setSessionManager(SessionManager sessionManager) {
        this.sessionManager = sessionManager;
    }
 
    public StringRedisTemplate getTemplate() {
        return template;
    }
 
    public void setTemplate(StringRedisTemplate template) {
        this.template = template;
    }
 
    private String getRedisKey(String key) {
        return REDIS_KEY_PREFIX + key;
    }
}
<!-- To configure securityManager -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
    <property name="cacheManager" ref="cacheManagerShiro"/>
    <property name="sessionManager" ref="sessionManager"/>
    <property name="realm" ref="jdbcRealm"/>
    <property name="authenticator.authenticationListeners">
        <list>
            <bean class="com.air.tqb.shiro.listener.ShiroAuthenticationListener"/>
            <bean class="com.air.tqb.shiro.listener.KickOutSessionListener">
                <property name="sessionManager" ref="sessionManager"/>
                <property name="kickoutAfter" value="false"/>
                <property name="maxSession" value="1"/>
            </bean>
        </list>
    </property>
</bean>

Whether there is a logged-in user check is put into shiro's successful logon callback to complete, and

Kickout Session Control Filter carries a simplified function of checking whether the user is kicked out.

At this time, the dependence and use of redis will be greatly reduced, and there will only be login-time validation. (In the future, when you need to access other login modes, you need to check whether the user is logged in or not.)

Posted by blt2589 on Fri, 28 Jun 2019 15:15:20 -0700