springboot + shiro实现帐号登录人数控制

业务场景

在某些项目中可能会遇到如每个账户同时只能有一个人登录或几个人同时登录,如果同时有多人登录:要么不让后者登录;要么踢出前者登录(强制退出)。

 

思路

Shiro没有提供默认实现,不过可以很容易的在Shiro中加入这个功能。通过Shiro Filter机制扩展自己的过滤器实现。

来看看AccessControlFilter

AccessControlFilter提供了访问控制的基础功能;比如是否允许访问/当访问拒绝时如何处理等:

isAccessAllowed:表示是否允许访问;mappedValue就是[urls]配置中拦截器参数部分,如果允许访问返回true,否则false

onAccessDenied:表示当访问拒绝时是否已经处理了;如果返回true表示需要继续处理;如果返回false表示该拦截器实例已经处理了,将直接返回即可。

 shiro拦截器机制可以看这篇文章:https://jinnianshilongnian.iteye.com/blog/2025656

 

代码实现思路

如果当前访问没有登录则直接放行;否则获取当前session信息,根据session获取redis中保存帐号sessionList队列,如果List中没有这个seesion信息且session状态没被T除则将当前session存入redis;接下在判断队列里面session个数如果大于1个则把先前session的状态改为已T除;这样当前一个session再次访问时直接告诉用户已被强制下线。

参考代码

https://github.com/gemingyi/shiro_demo/tree/master/shiro

代码实现

下面我们来看看自定义KickoutSessionControlFilter具体实现

public class KickoutSessionControlFilter extends AccessControlFilter {

    private String kickoutPrefix;

    private RedisTemplate redisTemplate;

    private SessionManager sessionManager;


    @Override
    protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) throws Exception {
        return false;
    }

    @Override
    protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        Subject subject = getSubject(servletRequest, servletResponse);
        //如果没有登录,不进行多出登录判断
        if (!subject.isAuthenticated() && !subject.isRemembered()) {
            return true;
        }

        Session session = subject.getSession();
        String username = (String) subject.getPrincipal();
        Serializable sessionId = session.getId();
        //获取redis中数据
        ArrayList<Serializable> deque = (ArrayList<Serializable>) redisTemplate.opsForList().range(kickoutPrefix + username, 0, -1);
        if (deque == null || deque.size() == 0) {
            deque = new ArrayList<>();
        }

        //如果队列里没有此sessionId,且用户没有被踢出,当前session放入队列
        if (!deque.contains(sessionId) && session.getAttribute("kickout") == null) {
            deque.add(sessionId);
            redisTemplate.opsForList().leftPush(kickoutPrefix + username, sessionId);
        }

        //如果队列里的sessionId数大于1,开始踢人
        while (deque.size() > 1) {
            //获取第一个sessionId(arrayList方法有限转成LinkedList)
            Serializable kickoutSessionId = (Serializable) new LinkedList(deque).removeFirst();
            deque.remove(kickoutSessionId);
            redisTemplate.opsForList().remove(kickoutPrefix + username, 1, kickoutSessionId);

            try {
                Session kickoutSession = sessionManager.getSession(new DefaultSessionKey(kickoutSessionId));
                //设置会话的kickout属性表示踢出了
                if (kickoutSession != null) {
                    kickoutSession.setAttribute("kickout", true);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        //session包含kickout属性,T出
        if (session.getAttribute("kickout") != null) {
            try {
                subject.logout();
            } catch (Exception e) {
                e.printStackTrace();
            }

            saveRequest(servletRequest);
            //返回401
            HttpServletResponse httpResponse = (HttpServletResponse) servletResponse;
            httpResponse.setStatus(HttpStatus.OK.value());
            httpResponse.setContentType("application/json;charset=utf-8");
            httpResponse.getWriter().write("{\"code\":" + CodeAndMsgEnum.UNAUTHENTIC.getcode() + ", \"msg\":\"" + "当前帐号在其他地方登录,您已被强制下载!" + "\"}");

            return false;

        }
        return true;
    }

    public void setKickoutPrefix(String kickoutPrefix) {
        this.kickoutPrefix = kickoutPrefix;
    }

    public void setSessionManager(SessionManager sessionManager) {
        this.sessionManager = sessionManager;

    }

    public void setRedisTemplate(RedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;

    }
}

ShiroConfiguration配置类

@Bean(name = "kickoutSessionControlFilter")
public KickoutSessionControlFilter jwtFilter(SessionManager sessionManager, RedisTemplate redisTemplate) {
    KickoutSessionControlFilter kickoutSessionControlFilter = new KickoutSessionControlFilter();
    kickoutSessionControlFilter.setSessionManager(sessionManager);
    kickoutSessionControlFilter.setRedisTemplate(redisTemplate);
    kickoutSessionControlFilter.setKickoutPrefix(kickoutPrefix);
    return kickoutSessionControlFilter;
}

@Bean(name = "shiroFilter")
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager, KickoutSessionControlFilter kickoutSessionControlFilter) {
    …
    //注意拦截链配置顺序 不能颠倒
    Map<String, String> filterChainDefinitionMap = new LinkedHashMap();
    …
    //拦截所有请求
    filterChainDefinitionMap.put("/**", "kickout,authc");
   );
    …
}

接口测试

先登录统一帐号2次登录

第一个session访问正常

看看redis中队列session

 

第二个session访问接口正常

 

再看看redis中的数据

 

第一个session再次访问

 

参考代码

1、第十八章 并发登录人数控制——《跟我学Shiro》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值