springboot+shiro自定义拦截器互踢问题

本文介绍了如何在Shiro框架中通过自定义拦截器`KickOutSessionControlFilter`实现会话互踢功能,确保同一账号在同一时刻只能在一个地方登录。拦截器关键在于`isAccessAllowed`和`onAccessDenied`方法的实现,通过缓存管理器和会话管理器判断并处理多个会话的情况。当会话数量超过预设最大值时,根据配置决定踢出先登录或后登录的用户。此外,还展示了如何在Spring配置中注册和使用这个拦截器。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

shiro自定义拦截器继承AccessControllerFilter,实现session互踢机制。
应用场景:
我们经常会有用到,当A 用户在北京登录 ,然后A用户在天津再登录 ,要踢出北京登录的状态。如果用户在北京重新登录,那么又要踢出天津的用户,这样反复。又或是需要限制同一用户的同时在线数量,超出限制后,踢出最先登录的或是踢出最后登录的。
分析:
spring security就直接提供了相应的功能;Shiro的话没有提供默认实现,不过可以很容易的在Shiro中加入这个功能。那就是使用shiro强大的自定义访问控制拦截器:AccessControlFilter,集成这个接口后要实现下面这2个方法:isAccessAllowed、onAccessDenied
isAccessAllowed:表示是否允许访问;mappedValue就是[urls]配置中拦截器参数部分,如果允许访问返回true,否则false;
onAccessDenied:表示当访问拒绝时是否已经处理了;如果返回true表示需要继续处理;如果返回false表示该拦截器实例已经处理了,将直接返回即可。
部分代码:

public class KickOutSessionControlFilter extends AccessControlFilter {

    private static final Logger logger = LoggerFactory.getLogger(KickOutSessionControlFilter.class);

    /**
     * 踢出之前登录的/之后登录的用户 默认踢出之前登录的用户
     */
    private boolean kickOutAfter = false;
    /**
     * 同一个帐号最大会话数 默认1
     */
    private int maxSession = 1;
    /**
     * 会话管理器
     */
    private SessionManager sessionManager;
    /**
     * 会话缓存
     */
    private Cache<String, Deque<Serializable>> cache;

    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(RedisCacheManager cacheManager) {
        this.cache = cacheManager.getCache("shiro_redis_cache");
    }

    /**
     * 是否允许访问,返回true表示允许
     *
     * @param servletRequest
     * @param servletResponse
     * @param obj
     * @return
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object obj) {
        return false;
    }

    /**
     * 表示访问拒绝时是否自己处理,如果返回true表示自己不处理且继续拦截器链执行,返回false表示自己已经处理了(比如重定向到另一个页面)。
     *
     * @param servletRequest
     * @param servletResponse
     * @return
     * @throws Exception
     */
    @Override
    protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        // 1同一个用户在不同ip上,不可以同时访问,后者会把前者踢出,即同一个用户不可以同时访问
        Subject subject = getSubject(servletRequest, servletResponse);
        System.out.println("===当前subject:==" + SecurityUtils.getSubject());
        if (!subject.isAuthenticated() && !subject.isRemembered()) {
            // 如果没有登录,直接进行之后的拦截器链
            return true;
        }
        // 当前用户
        User user = (User) subject.getPrincipal();
        String username = user.getUserName();
        
        // 当前会话
        Session session = subject.getSession();
        Serializable sessionId = session.getId();
        
        // 读取缓存用户 没有就存入
        Deque<Serializable> deque = cache.get(username);
        if (deque == null) {
            // 初始化队列
            deque = new ArrayDeque<Serializable>();
        }
        
        // 如果队列里没有当前会话sessionId,且当前会话未设置踢出标记(用户没有被踢出),放入队列
        if (!deque.contains(sessionId) && session.getAttribute("kickOut") == null) {
            // 将用户的sessionId存入队列
            deque.push(sessionId);
            // 将用户的sessionId存入队列缓存
            cache.put(username, deque);
        }
        
        // 如果队列里的sessionId数超出最大会话数,开始踢人
        while (deque.size() > maxSession) {
            Serializable kickOutSessionId = null;
            // 是否踢出后来登录的,默认是false,即后者登录的用户踢出前者登录的用户;
            if (kickOutAfter) {
                // 如果踢出后者
                kickOutSessionId = deque.removeFirst();
            } else {
                // 否则踢出前者
                kickOutSessionId = deque.removeLast();
            }
            // 踢出后再更新下缓存队列
            cache.put(username, deque);
            
            try {
                // 获取被踢出的sessionId的session对象
                Session kickOutSession = sessionManager.getSession(new DefaultSessionKey(kickOutSessionId));
                if (kickOutSession != null) {
                    // 设置会话的kickOut属性表示踢出了
                    kickOutSession.setAttribute("kickOut", true);
                    System.out.println("===将sessionId:==" + kickOutSession.getId() + "设置踢出标记");
                }
            } catch (Exception e) {
                // ignore exception
            }
        }
        // ajax请求,如果被踢出了,(前者或后者)直接退出,返回相应的状态
        if (session.getAttribute("kickOut") != null && (Boolean) session.getAttribute("kickOut") == true) {
            // 当前会话踢出标记不为空且等于true,会话被踢出了
            try {
                // 退出登录
                String ip = IPUtil.getIpAddress((HttpServletRequest) servletRequest);
                String url = ((HttpServletRequest) servletRequest).getRequestURL() + "";
                SecurityLogoutFilter.logout(subject);
                logger.info("IP地址为:" + ip + "的用户【" + username + "】被踢出,已在其他ip地址登录");
                ResponseUtil.returnResultAjax();
                return false;
            } catch (Exception e) {
                // ignore
            }
            return false;
        }
        return true;
    }
}

shiroFilterFactoryBean方法

		// 自定义过滤器
        Map<String, Filter> filters = new HashMap<>();
        // 同一用户登陆互踢
        filters.put("kickOut", kickOutSessionControlFilter());
		filterChainDefinitionMap.put("/**", "kickOut,authc");

定义拦截器的时候不需要加@Bean;

	//@Bean
    public KickoutSessionControlFilter kickoutSessionControlFilter(){
        KickoutSessionControlFilter kickoutSessionControlFilter = new KickoutSessionControlFilter();
        //用于根据会话ID,获取会话进行踢出操作的;
        //是否踢出后来登录的,默认是false;即后者登录的用户踢出前者登录的用户;
        kickoutSessionControlFilter.setKickoutAfter(false);
        //同一个用户最大的会话数,默认1;比如2的意思是同一个用户允许最多同时两个人登录;
        kickoutSessionControlFilter.setMaxSession(1);
        //被踢出后重定向到的地址
        kickoutSessionControlFilter.setKickoutUrl("/a/login");
        return kickoutSessionControlFilter;
    }

互踢分析:

1A用户第一次访问登录,进入互踢过滤器,获取当前会话,会话未认证不处理进入后面的拦截器,拦截器均未拦截住,进行正常登录获得会话token
2.1A用户第一次访问请求,进入互踢过滤器,获取当前会话,从cahe中获取deque,该deque不包含当前会话token且当前会话未设置标记,将会话token放入deque,放入cache,deque中token数量未超过1,当前会话的标记为空进入后面的拦截器
2.11A用户第二次访问请求,deque未变化,当前会话的标记为空进入后面的拦截器
2.2B用户第一次访问登录,进入互踢过滤器,获取当前会话,会话未认证不处理进入后面的拦截器,拦截器未拦截住,进行正常登录获得会话token
2.21B用户第一次访问请求,进入互踢过滤器,获取当前会话,从cache中获取deque,该deque不包含当前会话token且当前会话未设置标记,将会话token放入deque,放入cache,deque中token数量超过1,将deque中A的token删除,cache更新,将A的会话设置标记,当前会话的标记为空进入后面的拦截器
2.22B用户第二次访问请求,deque未变化,当前会话的标记为空进入后面的拦截器
3.1A用户第三次访问请求,进入互踢过滤器,获取当前会话,从cahe中获取deque,该deque不包含当前会话token但当前会话已设置标记不更新deque和cache,deque中的token数量未超过1,当前会话的标记不为空且为true,进行登出返回
3.2A用户第四次访问请求,会话过期请重新登录
4.1A用户第二次访问登录,进入进入互踢过滤器,获取当前会话,会话未认证不处理进入后面的拦截器,拦截器均未拦截住,进行正常登录获得会话token
4.2A用户第四次访问请求,进入互踢过滤器,获取当前会话,从cahe中获取deque,该deque不包含当前会话token且当前会话未设置标记,将会话token放入deque,放入cache,deque中token数量超过1,将deque中B的token删除,cache更新,将B的会话设备标记,当前会话的标记为空进入后面的拦截器
5B用户第三次访问请求,进入互踢过滤器,获取当前会话,从cahe中获取deque,该deque不包含当前会话token但当前会话已设置标记不更新deque和cache,deque中的token数量未超过1,当前会话的标记不为空且为true,进行登出返回
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值