SpringBoot Shiro实现并发登录人数控制(二)

1. 新增KickoutSessionControlFilter.java

package com.pk.ass.config;

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.SessionKey;
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 org.springframework.util.StringUtils;

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

/**
 * @author pk
 * @time 2020/11/03
 * @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> cache;

    /**
     * 是否允许访问,true为允许
     * @param servletRequest
     * @param servletResponse
     * @param o
     * @return
     * @throws Exception
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) 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();

        // doGetAuthenticationInfo方法配置的
        String userName = subject.getPrincipal().toString();
        Serializable sessionId = session.getId();

        // 初始化用户的队列放到缓存里
        Deque deque = cache.get(userName);
        if(deque == null) {
            deque = new LinkedList();
            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 {
                SessionKey sessionKey=new DefaultSessionKey(kickoutSessionId);
                Session kickoutSession = sessionManager.getSession(sessionKey);
                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;
    }

    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("dequeCache");
    }
}

2. 在ShiroConfig.java中增加

   /**
     * 并发登录控制
     * @return
     */
    @Bean
    public KickoutSessionControlFilter kickoutSessionControlFilter(){
        KickoutSessionControlFilter kickoutSessionControlFilter = new KickoutSessionControlFilter();

        //用于根据会话ID,获取会话进行踢出操作的;
        kickoutSessionControlFilter.setSessionManager(sessionManager());

        //使用cacheManager获取相应的cache来缓存用户登录的会话;用于保存用户—会话之间的关系的;
        kickoutSessionControlFilter.setCacheManager(ehCacheManager());

        //是否踢出后来登录的,默认是false;即后者登录的用户踢出前者登录的用户;
        kickoutSessionControlFilter.setKickoutAfter(false);

        //同一个用户最大的会话数,默认1;比如2的意思是同一个用户允许最多同时两个人登录;
        kickoutSessionControlFilter.setMaxSession(1);

        //被踢出后重定向到的地址;
        kickoutSessionControlFilter.setKickoutUrl("/sys/?kickout=true");

        return kickoutSessionControlFilter;
    }

3. 在ShiroConfig.java中的shiroFilterFactoryBean方法里增加

        //自定义拦截器限制并发人数
        LinkedHashMap filtersMap = new LinkedHashMap<>();

        //限制同一帐号同时在线的个数
        filtersMap.put("kickout", kickoutSessionControlFilter());

        shiroFilterFactoryBean.setFilters(filtersMap);

    //Filter工厂,设置对应的过滤条件和跳转条件
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        Map map = new HashMap<>();

        //自定义拦截器限制并发人数
        LinkedHashMap filtersMap = new LinkedHashMap<>();

        //限制同一帐号同时在线的个数
        filtersMap.put("kickout", kickoutSessionControlFilter());

        shiroFilterFactoryBean.setFilters(filtersMap);

        //退出
        map.put("/quitLogin", "logout");

        //配置static下面静态文件可以访问
        map.put("/statics/**", "anon");
        map.put("/sys/login", "kickout,anon");
        map.put("/sys/goRegister", "anon");
        map.put("/sys/register", "anon");
        map.put("/sys/goForget", "anon");
        map.put("/sys/changePassword", "anon");
        map.put("/sys/sendCode", "anon");
        map.put("/sysUser/checkUserName", "anon");

        //对所有用户认证
        map.put("/**", "kickout,user");

        //登录
        shiroFilterFactoryBean.setLoginUrl("/sys/");

        shiroFilterFactoryBean.setFilterChainDefinitionMap(map);

        return shiroFilterFactoryBean;
    }

4. ehcache-shiro.xml中增加

 
 
 

5. 跳转登录,接收kickout的参数 (kickout参数在第二步骤里面)

    @RequestMapping("/")
    public String goLogin(boolean kickout,ModelMap model){
        if(SecurityUtils.getSubject().getPrincipal() != "" && SecurityUtils.getSubject().getPrincipal() != null){
            return "/sys/sys_home";
        }
        if(kickout){
            model.addAttribute("kickout",kickout);
        }
        return "/sys/sys_login";
    }

6. 登录页面

//博主使用thymeleaf接收后台传值


7. 测试

打开两个浏览器分别登录同一个账号,博主使用的是火狐跟谷歌

SpringBoot Shiro实现并发登录人数控制(二)_第1张图片

参考链接

王赛超 - springboot整合shiro-在线人数以及并发登录人数控制(七)

你可能感兴趣的:(java,shiro,spring,boot)