Shiro功能应用(五)--Session管理的登陆人数控制


     登陆人数控制,比如同一个用户不能在两个地方登陆。Shiro主要基于自定义的Fliter实现的。本文在上一篇文章 Shiro功能应用(四)–Session管理及在线人数统计代码基础进行添加登陆人数控制。

代码实现:

      代码地址:
          https://github.com/OooooOz/SpringBoot-Shiro

     ShiroConfig的过滤器:

@Bean
	public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager securityManager){
		ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
		... ... ... ...
        LinkedHashMap<String, Filter> filtersMap = new LinkedHashMap<>();
        filtersMap.put("kickout", kickoutSessionControlFilter());       //限制同一帐号同时在线的个数
        shiroFilterFactoryBean.setFilters(filtersMap);

		//-----------------------------过虑器链定义------------------------------//
        LinkedHashMap<String, String> perms = new LinkedHashMap<>();
        //其他资源都需要认证  authc 表示需要认证才能进行访问 user表示配置记住我或认证通过可以访问的地址
        perms.put("/userList.do", "user,kickout");
	shiroFilterFactoryBean.setFilterChainDefinitionMap(perms);  //把权限过滤map设置shiroFilterFactoryBean
		return shiroFilterFactoryBean;
	}

     ShiroConfig的自定义人数控制过滤器:

 @Bean
    public KickoutSessionControlFilter kickoutSessionControlFilter(){
        KickoutSessionControlFilter kickoutSessionControlFilter = new KickoutSessionControlFilter();
        //用于根据会话ID,获取会话进行踢出操作的;
        kickoutSessionControlFilter.setSessionManager(sessionManager());
        //使用cacheManager获取相应的cache来缓存用户登录的会话;用于保存用户—会话之间的关系的;
        kickoutSessionControlFilter.setCacheManager(getEhCacheManager());
        //是否踢出后来登录的,默认是false;即后者登录的用户踢出前者登录的用户;
        kickoutSessionControlFilter.setKickoutAfter(false);
        //同一个用户最大的会话数,默认1;比如2的意思是同一个用户允许最多同时两个人登录;
        kickoutSessionControlFilter.setMaxSession(1);
        //被踢出后重定向到的地址;
        kickoutSessionControlFilter.setKickoutUrl("/toLogin.do?kickout=1");
        return kickoutSessionControlFilter;
    }

     自定义人数控制过滤器类:

package com.demo.config;

import java.io.Serializable;
import java.util.Deque;
import java.util.LinkedList;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;

import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheManager;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.DefaultSessionKey;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.AccessControlFilter;
import org.apache.shiro.web.util.WebUtils;

import com.demo.entity.User;

/**
 * @description: shiro 自定义filter 实现 并发登录控制
 */
public class KickoutSessionControlFilter  extends AccessControlFilter{

    /** 踢出后到的地址 */
    private String kickoutUrl;

    /**  踢出之前登录的/之后登录的用户 默认踢出之前登录的用户 */
    private boolean kickoutAfter = false;

    /**  同一个帐号最大会话数 默认1 */
    private int maxSession = 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-activeSessionCache");
    }
    // 是否允许访问,返回true表示允许
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
    	System.out.println("--------------------isAccessAllowed");
    	return false;
    }
    //表示访问拒绝时是否自己处理,如果返回true表示自己不处理且继续拦截器链执行,返回false表示自己已经处理了(比如重定向到另一个页面)。
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
    	System.out.println("--------------------onAccessDenied");
        Subject subject = getSubject(request, response);
        if(!subject.isAuthenticated() && !subject.isRemembered()) {
            //如果没有登录,直接进行之后的流程
            return true;
        }

        Session session = subject.getSession();
        String username = ((User) subject.getPrincipal()).getUserName();
        Serializable sessionId = session.getId();			//获取sessionId

        // 初始化用户的队列放到缓存里
        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.getFirst();
                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
                e.printStackTrace();
            }
        }

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

执行过程:

     1).访问http://localhost:8080/userList.do跳转登陆的时候,先执行认证的过滤器,即user对应的过滤器,然后再执行自定义的过滤器kickout(perms.put("/userList.do", “user,kickout”);过滤器链设置的)
     2).在kickout的过滤器中:
            如果isAccessAllowed返回false,则继续访问onAccessDenied
            如果onAccessDenied返回false,则表示在本方法已经处理,就不执行后续的/userList.do控制器处理
            如果isAccessAllowed返回true,则不继续访问onAccessDenied,直接执行后续控制器处理(当前过滤器结束)
在这里插入图片描述

     3).在onAccessDenied方法中:
            设置了EHCache缓存(kickoutSessionControlFilter.setCacheManager(getEhCacheManager());)第一个用户登陆,缓存无数据,deque为null,然后把这个用户队列put进缓存,后面把sessionId也push进用户队列deque里,第一个登陆的用户没有超过最大会话数,也不是踢出的会话。所以返回true去执行后续控制器逻辑。
在这里插入图片描述

            第二个用户登录的时候,也会把sessionId也push进用户队列deque里,但是超过了最大会话数量maxSession,就会将队列的sessionId移除( kickoutSessionId = deque.removeLast();返回移除的sessionId),然后被踢出队列的session设置会话属性(kickoutSession.setAttribute(“kickout”, true);),然后被踢出的session对应用户退出登录。
            被踢出用户再次访问时,由于session的会话属性不为null,所以无法将sessionId加入队列中,然后返回false,除非再次登陆,踢出其它登陆的session。
在这里插入图片描述
     参考文章
     springboot整合shiro-在线人数以及并发登录人数控制(七)

发布了17 篇原创文章 · 获赞 18 · 访问量 4550

猜你喜欢

转载自blog.csdn.net/weixin_43901882/article/details/105774672