《跟我学Shiro》笔记18-并发登录人数控制

  1. 配置使用(spring-config-shiro.xml)
  2. shiroFilter 配置
  3. KickoutSessionControlFilter

原文地址:第十八章 并发登录人数控制——《跟我学Shiro》
目录贴: 跟我学Shiro目录贴
源码:https://github.com/zhangkaitao/shiro-example

在某些项目中可能会遇到如每个账户同时只能有一个人登录或几个人同时登录,如果同时有多人登录:要么不让后者登录;要么踢出前者登录(强制退出)。比如 spring security 就直接提供了相应的功能;Shiro 的话没有提供默认实现,不过可以很容易的在 Shiro 中加入这个功能。

配置使用(spring-config-shiro.xml)

<bean id="kickoutSessionControlFilter"
class="com.github.zhangkaitao.shiro.chapter18.web.shiro.filter.KickoutSessionControlFilter">
    <property name="cacheManager" ref="cacheManager"/>
    <property name="sessionManager" ref="sessionManager"/>

    <property name="kickoutAfter" value="false"/>
    <property name="maxSession" value="2"/>
    <property name="kickoutUrl" value="/login?kickout=1"/>
</bean>
  1. cacheManager:使用 cacheManager 获取相应的 cache 来缓存用户登录的会话;用于保存用户—会话之间的关系的;
  2. sessionManager:用于根据会话 ID,获取会话进行踢出操作的;
  3. kickoutAfter:是否踢出后来登录的,默认是 false;即后者登录的用户踢出前者登录的用户;
  4. maxSession:同一个用户最大的会话数,默认 1;比如 2 的意思是同一个用户允许最多同时两个人登录;
  5. kickoutUrl:被踢出后重定向到的地址。

shiroFilter 配置

<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
     <property name="securityManager" ref="securityManager"/>
     <property name="loginUrl" value="/login"/>
     <property name="filters">
         <util:map>
             <entry key="authc" value-ref="formAuthenticationFilter"/>
             <entry key="sysUser" value-ref="sysUserFilter"/>
             <entry key="kickout" value-ref="kickoutSessionControlFilter"/>
         </util:map>
     </property>
     <property name="filterChainDefinitions">
         <value>
             /login = authc
             /logout = logout
             /authenticated = authc
             /** = kickout,user,sysUser
         </value>
     </property>
 </bean>

除了登录等之外的地址都走 kickout 拦截器进行并发登录控制

KickoutSessionControlFilter

public class KickoutSessionControlFilter extends AccessControlFilter {

    private String kickoutUrl; // 踢出后到的地址
    private boolean kickoutAfter = false; // 踢出之前登录的/之后登录的用户 默认踢出之前登录的用户
    private int maxSession = 1; // 同一个帐号最大会话数 默认 1

    private SessionManager sessionManager;
    private Cache<String, Deque<Serializable>> cache;

    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;
    }

    public void setCacheManager(CacheManager cacheManager) {
        this.cache = cacheManager.getCache("shiro-kickout-session");
    }

    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
        return false;
    }

    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        Subject subject = getSubject(request, response);
        if(!subject.isAuthenticated() && !subject.isRemembered()) {
            // 如果没有登录,直接进行之后的流程
            return true;
        }

        Session session = subject.getSession();
        String username = (String) subject.getPrincipal();
        Serializable sessionId = session.getId();

        // 同步控制
        Deque<Serializable> deque = cache.get(username);
        if(deque == null) {
            deque = new LinkedList<Serializable>();
            cache.put(username, deque);
        }

        // 如果队列里没有此 sessionId,且用户没有被踢出;放入队列
        if(!deque.contains(sessionId) && session.getAttribute("kickout") == null) {
            deque.push(sessionId);
        }

        // 如果队列里的 sessionId 数超出最大会话数,开始踢人
        while(deque.size() > maxSession) {
            Serializable kickoutSessionId = null;
            if(kickoutAfter) { // 如果踢出后者
                kickoutSessionId = deque.removeFirst();
            } else { // 否则踢出前者
                kickoutSessionId = deque.removeLast();
            }
            try {
                Session kickoutSession = sessionManager.getSession(new DefaultSessionKey(kickoutSessionId));
                if(kickoutSession != null) {
                    // 设置会话的 kickout 属性表示踢出了
                    kickoutSession.setAttribute("kickout", true);
                }
            } catch (Exception e) { // ignore exception
            }
        }

        // 如果被踢出了,直接退出,重定向到踢出后的地址
        if (session.getAttribute("kickout") != null) {
            // 会话被踢出了
            try {
                subject.logout();
            } catch (Exception e) { // ignore
            }
            saveRequest(request);
            WebUtils.issueRedirect(request, response, kickoutUrl);
            return false;
        }
        return true;
    }
}

此处使用了 Cache 缓存用户名—会话 id 之间的关系;如果量比较大可以考虑如持久化到数据库/其他带持久化的 Cache 中;另外此处没有并发控制的同步实现,可以考虑根据用户名获取锁来控制,减少锁的粒度。


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 bin07280@qq.com

文章标题:《跟我学Shiro》笔记18-并发登录人数控制

文章字数:975

本文作者:Bin

发布时间:2018-04-21, 20:12:22

最后更新:2019-09-03, 16:52:46

原始链接:http://coolview.github.io/2018/04/21/%E8%B7%9F%E6%88%91%E5%AD%A6Shiro/%E3%80%8A%E8%B7%9F%E6%88%91%E5%AD%A6Shiro%E3%80%8B%E7%AC%94%E8%AE%B018-%E5%B9%B6%E5%8F%91%E7%99%BB%E5%BD%95%E4%BA%BA%E6%95%B0%E6%8E%A7%E5%88%B6/

版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。

目录