首页 > 其他分享 >shiro实现用户踢出,在线用户列表展示功能,包含常见踩坑集合、代码下载

shiro实现用户踢出,在线用户列表展示功能,包含常见踩坑集合、代码下载

时间:2024-01-20 22:08:27浏览次数:18  
标签:return 登录 用户 列表 session 监听器 shiro subject


功能描述:用户a登录了s账号,接着用户b也登录了s账号,此时用户a将被踢出。一个账号只能一个人登录,被别人登录了,那么你就要被踢下线。


本文目录

  • shiro认证与授权理解
  • 实现需求核心
  • 以下是实现shiro用户踢出
  • KickOutListener(登录成功后加入业务逻辑)
  • kickOutFilter(进入controller的初级验证)
  • 配置监听器、过滤器遇到的坑集合!!!
  • 坑一: 监听器中的RedisTemplate注入不进来一直为null。
  • 坑二:说realm没有配置
  • 坑三:过滤器无效
  • 坑四:不知道如何注入监听器
  • 然后就是测试
  • 吹毛求疵的优化
  • 以下实现在线用户列表展示需求
  • 添加session监听器


shiro认证与授权理解

用户在登录的时候只执行认证方法而没有马上去执行授权doGetAuthorizationInfo()方法。

  • shiro并不是在认证之后就马上对用户授权,而是在用户认证通过之后,接下来要访问的资源或者目标方法需要权限的时候才会调用doGetAuthorizationInfo()方法,进行授权.
  • 比如当认证通过后,访问@RequiresPermissions注解的目标方法,或者目标页面中有shiro的权限标签,这是shiro就会调用doGetAuthorizationInfo()方法.

实现需求核心

  • 理解: 在loginOut的同时会删除session,退出浏览器不会删除session(我个人是配置了redis缓存session还有realm的)可以参考这篇博客shiro配置安全管理器之缓存管理器详解(底层用redis做存储,实现分布式session及分布式授权访问)
  • AccessControlFilter(进入controller的过滤器):访问一些不需要权限的接口或者url时会进入AccessControlFilter进行过滤,当访问需要权限的接口或者url不会进入AccessControlFilter进行过滤,此时就会直接走授权。(doGetAuthorizationInfo),看是否有无权限进入此接口或者url。
  • AuthenticationListener(认证监听器):通过实现这个接口我们可以在登录成功、失败、登出时进行一些对应的操作
  • SessionListenerAdapter(session的监听器):通过继承这个接口,可以在session创建后、销毁后、超时后,写一些我们自己的业务逻辑。

以下是实现shiro用户踢出

KickOutListener(登录成功后加入业务逻辑)

往当前登录成功的session中存放一个当前user的唯一标识(方便在线用户列表功能的实现),登录成功且当前登录的用户没有被踢出那么把当前登录用户的sessionId作为value值存入redis的list集合中,key为当前登录用户的用户名。如果list的大小超过了1,那么踢出list之前的sessionId,进而拿到被剔出的session,往被踢的session中设置一个kisckOut的值。

/**
 * @author 张子行
 * @class 踢出监听器
 */
@Slf4j
public class KickOutListener implements AuthenticationListener {
    private String KICK_OUT_KEY = "ZZH_KICK_OUT";
    private String REDIS_LIST_KEY = null;
    //最大人数限制
    private Integer MAX_LOGIN_NUM = 1;
    private Boolean enbleKickOut = true;
    //默认当前用户登录了,是把上一个用户给踢掉
    private Boolean kickOutOrder = true;
    @Autowired
    private RedisTemplate redisTemplate;
    @Autowired
    private SessionManager sessionManager;

    /**
     * @param
     * @method shiro登录成功后调用
     */
    @Override
    public void onSuccess(AuthenticationToken token, AuthenticationInfo info) {
        //登录成功后的subject
        Subject subject = SecurityUtils.getSubject();
        ListOperations redisList = redisTemplate.opsForList();
        Session session = subject.getSession();
        session.setAttribute("loginUserName", info.getPrincipals().getPrimaryPrincipal());
        Session kickOutSession;
        Integer sessionId = (Integer) session.getId();
        Object kickOutSessionId = null;
        User user = new User();
        try {
            BeanUtils.copyProperties(user, info.getPrincipals().getPrimaryPrincipal());
            REDIS_LIST_KEY = user.getUsername();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }

        //第一次登录,从当前session中获取KICK_OUT_KEY值
        if (session.getAttribute(KICK_OUT_KEY) == null && REDIS_LIST_KEY != null) {
            redisList.leftPush(REDIS_LIST_KEY, sessionId);
        }

        //队列里面超过MAX_LOGIN_NUM,开始踢人
        //enbleKickOut也算一个扩展点叭,默认是true,可以踢人,当然自己也可以进行扩展设置
        if (redisList.size(REDIS_LIST_KEY) > MAX_LOGIN_NUM && enbleKickOut) {
            if (kickOutOrder) {
                //踢出,返回值是踢出的value
                kickOutSessionId = redisList.rightPop(REDIS_LIST_KEY);
            } else {
                kickOutSessionId = redisList.leftPop(REDIS_LIST_KEY);
            }
        }
        try {
            //获取被踢出用户的session
            kickOutSession = sessionManager.getSession(new DefaultSessionKey((Serializable) kickOutSessionId));
            kickOutSession.setAttribute(KICK_OUT_KEY, true);
            log.info("踢出用户成功");
        } catch (Exception e) {
            log.info("踢出用户出错");
        }
        log.info("监听登录成功");
    }

    public KickOutListener setEnbleKickOut(Boolean enbleKickOut) {
        this.enbleKickOut = enbleKickOut;
        return this;
    }

    /**
     * @param
     * @method shiro登录失败后调用
     */
    @Override
    public void onFailure(AuthenticationToken token, AuthenticationException ae) {
        log.info("监听登录失败");
    }

    /**
     * @param
     * @method shiro登出后调用
     */
    @Override
    public void onLogout(PrincipalCollection principals) {
        log.info("监听出成功");
    }
}

kickOutFilter(进入controller的初级验证)

根据当前的servletRequest、servletResponse获取里面的subject,进而获取subject中的session。如果此session里面有kickOut的值存在,登出当前用户,且跳转到登录页面。

/**
 * @author 张子行
 * @class 踢出过滤器
 */
@Slf4j
public class kickOutFilter extends AccessControlFilter {
    private String KICK_OUT_URL;
    private String KICK_OUT_KEY = "ZZH_KICK_OUT";

    public kickOutFilter setKICK_OUT_URL(String KICK_OUT_URL) {
        this.KICK_OUT_URL = KICK_OUT_URL;
        return this;
    }

    /**
     * @param
     * @method return false接着进入onAccessDenied,反之不会
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) throws Exception {
        log.info("isAccessAllowed");
        return false;
    }

    /**
     * @param
     * @method return false直接给浏览器响应,return true正常请求
     */
    @Override
    protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        log.info("onAccessDenied");
        Subject subject = getSubject(servletRequest, servletResponse);
        User user = new User();
        Session session;
        try {
            BeanUtils.copyProperties(user,subject.getPrincipals().getPrimaryPrincipal());
            if (!subject.isAuthenticated() && !subject.isRemembered()) {
                //如果没有登录,不做处理直接放行
                return true;
            }
        } catch (Exception e) {
            System.out.println(e);
            //可能此次操作之前并没有进行过登录,那么subject.getPrincipals().getPrimaryPrincipal()此时为null会出现异常,直接放行就行
            return true;
        }
        //当前登录的subject的session
        session = subject.getSession();
        if (user == null || session == null) {
            //如果此次操作的subject中都没有userName、session的值,也不做处理直接放行
            return true;
        }
        if (session.getAttribute(KICK_OUT_KEY) != null) {
            subject.logout();

            //此次会话已经被踢出,跳转到踢出页面
            WebUtils.issueRedirect(servletRequest, servletResponse, KICK_OUT_URL);
            return false;
        }
        return true;
    }
}

配置监听器、过滤器遇到的坑集合!!!

坑一: 监听器中的RedisTemplate注入不进来一直为null。

原因:监听器加载于IOC之前,所以这个时候注入RedisTemplate时是null

解决办法: 将自定义监听器也加入到配置中,在拦截器执行的时候实例化监听器Bean

坑二:说realm没有配置

报如下错误:Configuration error: No realms have been configured! One or more
realms must be present to execute an authorization
operation.java.lang.IllegalStateException: Configuration error: No
realms have been configured! One or more realms must be present to
execute an authorization operation.()

原因: 还不清除,可能和shiro的源码有关,会解决就谢谢了!

解决办法: 配置SecurityManager的时候,这里切记先添加Authenticator,在添加realm,否则报错。其他的顺序都会出岔子我也是醉了。感谢

/**
     * @param
     * @method 配置监听器bean
     */
    @Bean
    public AuthenticationListener KickOutListener() {
        return new KickOutListener();
    }
   /**
     * @param
     * @method 这里设置了shiroCacheManager,表示要缓存loginRealm,以及会话session。
     * 这里如果不设置shiroCacheManager,由于shiroSessionDao(shiroCacheManager)已经注入过shiroCacheManager
     * session信息也是能被缓存的
     */
    @Bean
    public SecurityManager securityManager() {
        ArrayList<AuthenticationListener> listeners = new ArrayList<>();
        ModularRealmAuthenticator modularRealmAuthenticator = new ModularRealmAuthenticator();
        //添加监听器
        listeners.add(kickOutListener());
        modularRealmAuthenticator.setAuthenticationListeners(listeners);

        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // 缓存授权信息,也就是realm
        // securityManager.setCacheManager(shiroCacheManager);
        securityManager.setSessionManager(sessionManager());
        //这里切记先添加modularRealmAuthenticator,在添加loginRealm(),否则报错
        //Configuration error: No realms have been configured! One or more realms must be present to execute an authorization operation.java.lang.IllegalStateException: Configuration error:  No realms have been configured!  One or more realms must be present to execute an authorization operation.
        securityManager.setAuthenticator(modularRealmAuthenticator);
        securityManager.setRealm(loginRealm());
        return securityManager;
    }

坑三:过滤器无效

我们注册了过滤器但是也要同时指定这个过滤器作用于哪些路径。

shiro实现用户踢出,在线用户列表展示功能,包含常见踩坑集合、代码下载_redis

坑四:不知道如何注入监听器

看网上都是用xml配置的,并没有用springboot的配置,自己也是逐个看了一下SecurityManager下面有哪些方法,然后就知道怎么注入监听器咯。

shiro实现用户踢出,在线用户列表展示功能,包含常见踩坑集合、代码下载_redis_02

然后就是测试

打开2个浏览器登录同一个账号,后登录的会把之前登录的给踢出,然后先前登录的刷新页面会重新跳转到登录页面。

shiro实现用户踢出,在线用户列表展示功能,包含常见踩坑集合、代码下载_数据库_03


shiro实现用户踢出,在线用户列表展示功能,包含常见踩坑集合、代码下载_分布式_04


shiro实现用户踢出,在线用户列表展示功能,包含常见踩坑集合、代码下载_数据库_05

吹毛求疵的优化

a用户把b用户给踢出了,但是如果此时b用户所在的界面是一个权限操作页面(这个页面中的任何一个按钮都是需要权限才能操作成功的)呢?b用户虽然被踢出了,但是此时b用户接着在这个页面上操作,或者是重新刷新此页面,并不会进入kickOutFilter,而是直接走realm中的授权 public AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals),先授权,看是否有权限进行页面中的权限操作,而且此时的PrincipalCollection principals默认是被shiro缓存起来的,也就是说此时即使b用户被踢出了,但是授权还是可以继续成功的,在此时这个页面还是可以继续进行权限的操作,并不会走kickOutFilter,这不是有悖踢出嘛。

解决办法:

大前提:关闭自动缓存realm,如果缓存realm那么你就永远都执行不到授权的代码了,会直接从缓存中拉取realm

shiro实现用户踢出,在线用户列表展示功能,包含常见踩坑集合、代码下载_分布式_06


在授权这加一个判断,如果之前登录的subject中的session中拿得到KICK_OUT_KEY的值,直接登出,且返回一个空的权限的SimpleAuthorizationInfo 。这样此时b用户就啥也干不了了。

/**
     * @param
     * @return
     * @function 授权
     */

    @Override
    public AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        Subject subject = SecurityUtils.getSubject();
        Session session = subject.getSession();
        if (session.getAttribute(KICK_OUT_KEY) != null) {
            subject.logout();
            log.info("被踢出的sessionId" + session.getId());
            //此次会话已经被踢出,跳转到踢出页面
            SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
            return simpleAuthorizationInfo;
        }
        return Authorization(principals);
    }

    private SimpleAuthorizationInfo Authorization(PrincipalCollection principals) {
        log.info("授权。。。。。");
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        ArrayList<String> perms = new ArrayList<>();
        Object key = principals.getPrimaryPrincipal();
        User loginUser = new User();
        try {
            BeanUtils.copyProperties(loginUser, key);
        } catch (Exception e) {
        }
        //TODO这里是模拟数据库中的权限表的情况
        if (loginUser.getUsername().equals("shiro")) {
            perms.add("shiro");
            simpleAuthorizationInfo.addRole("shiro");
            simpleAuthorizationInfo.addStringPermissions(perms);
        }
        if (loginUser.getUsername().equals("zzh")) {
            simpleAuthorizationInfo.addRole("index");
            perms.add("index");
            simpleAuthorizationInfo.addStringPermissions(perms);
        }
        log.info("授权完成。。。。。");
        return simpleAuthorizationInfo;
    }

以下实现在线用户列表展示需求

也可以参考这片文章数据库中添加用户是否登录is_login字段,用来记录用户的登录状态,假设1表示在线;0表示未在线,则需要进行控制:

1)当用户登录成功时,设置用户登录状态为1

2)当用户退出登录时,设置用户登录状态为0

3)当session失效时,设置对应的用户的登录状态为0

自定义一个类ShiroSessionLinstener,继承SessionLinstenerAdapter类,同时重写监听函数

1)session创建时,输出sessionId,不进行任何操作

2)session停止时,修改数据库当前用户的登录状态字段为0

3)session失效时,修改数据库当前用户的登录状态字段为0

public class ShiroSessionListener extends SessionListenerAdapter {
    // session创建
    @Override
    public void onStart(Session session) {
        super.onStart(session);
        System.out.println("session创建,sessionId:" + session.getId());
    }

    // session停止
    @Override
    public void onStop(Session session) {
        //TODO 修改数据库当前用户的登录状态字段
        //登录成功后设置过这个Attribute的
        User loginUser = (User) session.getAttribute("loginUserName");
        System.out.println("session停止,sessionId:" + session.getId() + ",用户:" + loginUser);
    }

    // session失效
    @Override
    public void onExpiration(Session session) {
        //TODO 修改数据库当前用户的登录状态字段
        //登录成功后设置过这个Attribute的
        User loginUser = (User) session.getAttribute("loginUserName");
        System.out.println("session失效,sessionId:" + session.getId() + ",用户:" + loginUser);
    }

}

添加session监听器

SessionManager中配置即可

/**
     * @param
     * @method DefaultWebSessionManager的配置,设置了sessionDAO
     */
    @Bean
    public SessionManager sessionManager() {
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        ArrayList<SessionListener> listeners = new ArrayList<>();
        listeners.add(ShiroSessionListener());
        sessionManager.setSessionListeners(listeners);
        sessionManager.setSessionDAO(sessionDAO());
        return sessionManager;
    }

点我获取完整代码


标签:return,登录,用户,列表,session,监听器,shiro,subject
From: https://blog.51cto.com/u_16414043/9346251

相关文章

  • shiro缓存配置流程
    以前搞shiro的时候没有刻意去研究过这些配置文件,导致用shiro的时候也是迷迷糊糊,惭愧啊,要想成为人上人,读源码,懂配置是真滴重要!安全管理器配置(SecurityManager)配置项意思一setCacheManager配置缓存管理器用来缓存realm和session信息二setRealm登录时假如用的UsernamePasswordToken,......
  • sringboot整合shiro实现前后端鉴权控制,标签注解速成(包含常见错误的出现,前后端注解标签
    搭建shiro环境1:导入boot项目中要用到的shiro依赖<!--shiro部分--><!--shiro核心源码--><dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-spring</artifactId><version......
  • Linux用户管理小记
    1.用户分类系统中三类用户:UID方式识别用户userid UID第一类:管理员0root最高权限第二类:虚拟用户1-999系统运行程序必须有一个用户来支持,用户不需要登录系统第三类:普通用户1000+常用个人用户为了提高系统安全性企业都使用普通用户......
  • Feign源码解析6:如何集成discoveryClient获取服务列表
    背景我们上一篇介绍了feign调用的整体流程,在@FeignClient没有写死url的情况下,就会生成一个支持客户端负载均衡的LoadBalancerClient。这个LoadBalancerClient可以根据服务名,去获取服务对应的实例列表,然后再用一些客户端负载均衡算法,从这堆实例列表中选择一个实例,再进行http调用即......
  • 用户需求调查单
    ......
  • 用户需求说明书
    ......
  • 集成微软 Clarity 项目用户前端埋点
    ......
  • Windows server 2022中 curl命令参数完整列表
    用法:curl[选项...]<url>--abstract-unix-socket<path>通过抽象的Unix域套接字进行连接--alt-svc<filename>启用带有此缓存文件的alt-svc--anyauth选择任何身份验证方法-a,--append在上传时将数据追加到目标文件--aws-sigv4<provider1[:provider2[:region[:service]]]>......
  • 安全基础-用户和组管理
    1用户管理1.1用户概述每一个用户登录系统后,拥有不同的操作权限每个账户有自己唯一的SID(安全标识符)用户SID:S-1-5-21-426206823-2579496042-14852678-500系统SID:S-1-5-21-426206823-2579496042-14852678用户UID:500WIndows系统管理员Administrator的UID是500普通用户的......
  • python之列表
    列表详解                     1.appenddefappend(self,*args,**kwargs):#realsignatureunknown"""Appendobjecttotheendofthelist."""pass翻译:在列表的最后加追加对象1#!/usr/bin/python2test=[1,2,3......