简介

  • 在某些项目中,一个账户可能只允许一人登录或几个人同时登录,当超过人数时,要么进制后者登录,要么踢出前者登录。
  • Spring Security直接提供了相应的功能。
  • Shiro没有提供默认实现,但自己添加也是很容易的。

示例

  • 采用第十六章的示例,主要功能是用户、公司、角色和资源之间关系的调整,如修改用户所属公司、所有角色,资源所需权限、角色所有资源等。
  • 目的:添加登录人数限制功能。
  • KickoutSessionControlFilter:控制登录人数的过滤器。
public class KickoutSessionControllerFilter extends AccessControlFilter {
    //踢出后重定向的地址
    private String kickoutUrl;
    //是否踢出之后登录的用户,默认false,即踢出之前登录的而用户
    private boolean kickoutAfter=false;
    //同一账号最大会话数,默认1
    private int maxSession=1;
    //根据会话id,获取会话进行踢出操作
    private SessionManager sessionManager;
    //使用cacheManager获取相应cache来缓存用户登录的会话,用于保存用户-会话之间的关系
    private Cache<String,Deque<Serializable>> cache;

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

    public void setCacheManager(CacheManager cacheManager) {
        //根据name获取cache实例,没有则新建一个
        this.cache = cacheManager.getCache("shiro-kickout-session");
    }

    //其他setter

    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response)
            throws Exception {
        Subject subject=getSubject(request,response);

        //如果未登录,则直接进行之后的流程
        if(!subject.isAuthenticated()&&!subject.isRemembered())
            return true; //打断点,看到底和false什么区别!!!

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

        //获取之前登录者队列
        Deque<Serializable> deque=cache.get(username); //若有多人登录,
            //则查询key为username的一条目,其value为包含多个会话的队列
        if(deque==null){
            deque=new LinkedList<>();
            //第一次暂时存储了一个空队列进去,但下一个if将放入会话,
            //所以第一次存储了一个包含自身会话的队列
            cache.put(username,deque); //存储时键为username,值为之前登录者会话队列
        }

        //如果队列里没有此sessionId,且用户没有被踢出,则放入队列
        if(!deque.contains(sessionId) && session.getAttribute("kickout")==null)
            deque.push(sessionId); //插入队头,而队列的引用放在cache中,相当于缓存会话

        //如果队列里的会话数超出最大值,则开始踢人
        while (deque.size()>maxSession){ //用if应该就可以,顶多加入当前用户即超出限制
            //要被踢的sessionId
            Serializable kickoutSessionId=null;
            //如果规定踢出后者
            if(kickoutAfter)
                kickoutSessionId=deque.removeFirst(); //弹出首元素,此处为最后进去的元素
            //否则踢出前者
            else
                kickoutSessionId=deque.removeLast(); //弹出尾元素
            try {
                //根据session id获取session
                Session kickoutSession = sessionManager
                        .getSession(new DefaultSessionKey(kickoutSessionId));
                if (kickoutSession != null)
                    //设置会话的kickout属性表示被踢出
                    kickoutSession.setAttribute("kickout", true);
            } catch (Exception e){
                //ignore
            }
        }

        //如果当前会话不幸在上面while中被踢出了,则注销并重定向到踢出后的地址;
        //但如果踢出的是其他会话,那么需要刷新,它才会被强制登出
        if(session.getAttribute("kickout")!=null){
            try {
                subject.logout();
            } catch (Exception e){
                //ignore
            }
            saveRequest(request); //保存当前请求
            WebUtils.issueRedirect(request,response,kickoutUrl); //返回login页面
            return false;
        }
        return true;
    }
}
  • ehcache.xml:设置用于存放会话的缓存shiro-kickout-session。

    <cache name="shiro-kickout-session"
       maxEntriesLocalHeap="2000"
       eternal="false"
       timeToIdleSeconds="3600"
       timeToLiveSeconds="0"
       overflowToDisk="false"
       statistics="true">
    </cache>
    
  • spring-config-shiro.xml: 注册过滤器。

    <!--控制并发登录人数的过滤器-->
    <bean id="kickoutSessionControllerFilter"
          class="com.haien.chapter18.web.shiro.filter.KickoutSessionControllerFilter">
        <!--使用cacheManager获取相应cache来缓存用户登录的会话,
        用于保存用户-会话之间的关系-->
    
        <property name="cacheManager" ref="cacheManager"/>
        <!--用于根据会话id,获取会话进行踢出操作-->
        <property name="sessionManager" ref="sessionManager"/>
        <!--是否踢出后来登录的,默认false-->
        <property name="kickoutAfter" value="false"/>
        <!--同一个用户最大会话数,默认1,即只能一人登录-->
        <property name="maxSession" value="2"/>
        <!--被踢出后重定向到的地址-->
        <property name="kickoutUrl" value="/login?kickout=1"/>
    </bean>
    
    <!-- Shiro的Web过滤器 -->
    <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="kickoutSessionControllerFilter"/>
            </util:map>
        </property>
        <property name="filterChainDefinitions">
            <value>
                /login = authc
                /logout = logout
                /authenticated = authc
                /** = kickout,user,sysUser
            </value>
        </property>
    </bean>
    
  • 测试:本例设置maxSession=2,所以需要打开3个浏览器,分别访问localhost:8080/chapter18进行登录,然后刷新第一次登录的浏览器,将会被强制退出返回登录页面。

  • 《跟我学Shiro》第十八章

  • 代码示例:ideaProjects/shiro-chapter18