springboot2+shiro+redis限制同一账号同时在线人数

2023-11-03

springboot2+shiro+redis限制同一账号同时在线人数

我们在写系统的时候,需要注意账号安全问题,最好的处理方法就是同一个账号只能在一个地方登录。

原理

大概的原理就是每次登录的时候将登录的sessionId存入缓存,然后登录之前或者在做任何操作之前去读取这个缓存里面是否还保存有这个登录的sessionId,如果没有了说明被踢下线了,直接跳转到登录页面。(大概是这个逻辑哈,我也没深究)

代码

话不多说,上代码:

拦截器

这个网上很多版本,我随便找的一个版本自己改的。

import java.io.IOException;
import java.io.PrintWriter;
import java.io.Serializable;
import java.util.Deque;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
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.alibaba.fastjson.JSON;
import com.qk.common.util.BaseResultUtils;
import com.qk.platform.database.model.HtUser;
/**
 * @时间:2023年7月17日 上午10:31:51
 * @作者:秦二少
 * @描述:限制同一个账号同时在线人数拦截器
 */
public class KickoutSessionControlFilter extends AccessControlFilter {
    private String kickoutUrl; //踢出后到的地址
    private boolean kickoutAfter = false; //踢出之前登录的/之后登录的用户 默认踢出之前登录的用户
    private int maxSession = 1; //同一个帐号最大会话数 默认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;
    }
    //设置Cache的key的前缀
    public void setCacheManager(CacheManager cacheManager) {
        this.cache = cacheManager.getCache("shiro_redis_cache");
    }

    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) 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();
        HtUser user = (HtUser) subject.getPrincipal();
        String username = user.getUserName();
        Serializable sessionId = session.getId();
        //读取缓存   没有就存入
        Deque<Serializable> deque = cache.get(username);
        //如果此用户没有session队列,也就是还没有登录过,缓存中没有
        //就new一个空队列,不然deque对象为空,会报空指针
        if(deque==null){
            deque = new LinkedList<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;
            if(kickoutAfter) { //如果踢出后者
                kickoutSessionId = deque.removeFirst();
                //踢出后再更新下缓存队列
                cache.put(username, deque);
            } else { //否则踢出前者
                kickoutSessionId = deque.removeLast();
                //踢出后再更新下缓存队列
                cache.put(username, deque);
            }
            try {
                //获取被踢出的sessionId的session对象
                Session kickoutSession = sessionManager.getSession(new DefaultSessionKey(kickoutSessionId));
                //这里向上一个登录此账号的前端发送提示消息
                WebSocketServer.sendMessage(BaseResultUtils.sendMsg("loginOut", "您的账号在其他地方登录,您已被挤下线!如果不是您在登录,请立即修改密码!"),user.getId()+"");
                if(kickoutSession != null) {
                    //设置会话的kickout属性表示踢出了
                    kickoutSession.setAttribute("kickout", true);
                }
            } catch (Exception e) {//ignore exception
            }
        }
        //如果被踢出了,直接退出,重定向到踢出后的地址
        if (session.getAttribute("kickout") != null) {
            //会话被踢出了
            try {
                //退出登录
                subject.logout();
            } catch (Exception e) { //ignore
            }
            saveRequest(request);
            Map<String, String> resultMap = new HashMap<String, String>();
            //判断是不是Ajax请求
            if ("XMLHttpRequest".equalsIgnoreCase(((HttpServletRequest) request).getHeader("X-Requested-With"))) {
                resultMap.put("user_status", "300");
                resultMap.put("message", "您已经在其他地方登录,请重新登录!");
                //输出json串
                out(response, resultMap);
            }else{
                //重定向
                WebUtils.issueRedirect(request, response, kickoutUrl);
            }
            return false;
        }
        return true;
    }
    private void out(ServletResponse hresponse, Map<String, String> resultMap)
            throws IOException {
        try {
            hresponse.setCharacterEncoding("UTF-8");
            PrintWriter out = hresponse.getWriter();
            out.println(JSON.toJSONString(resultMap));
            out.flush();
            out.close();
        } catch (Exception e) {
            System.err.println("KickoutSessionFilter.class 输出JSON异常,可以忽略。");
        }
    }
}

这里要注意的是我在踢出登录之前,给上一个登录的人发了一个提示的:
WebSocketServer.sendMessage(BaseResultUtils.sendMsg(“loginOut”, “您的账号在其他地方登录,您已被挤下线!如果不是您在登录,请立即修改密码!”),user.getId()+“”);
我这里是用的websocket方法(具体可以看我另外一篇文章),这就会实现 你这里一登录,上一个登录那边就会马上弹出一个提示框,提示他被挤下线了,并且会自动跳转到登录页面。

shiro配置类

这里才是重点,这个配置类网上也有很多版本,下面是我自己的版本

import java.util.LinkedHashMap;
import java.util.Map;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.crazycake.shiro.RedisCacheManager;
import org.crazycake.shiro.RedisManager;
import org.crazycake.shiro.RedisSessionDAO;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;

@Configuration
public class ShiroConfig {
	/**
     * 身份认证realm; (自己写的账号密码校验;权限等)
     */
    @Bean
    public MyShiroRealm myShiroRealm() {
        MyShiroRealm myShiroRealm = new MyShiroRealm();
        myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
        return myShiroRealm;
    }
    
    /**
	 * @类名: ShiroConfig.java 
	 * @时间: 2021年12月6日 下午4:37:42 
	 * @作者: 秦二少
	 * @return
	 * @描述: 权限管理,配置主要是Realm的管理认证
	 */
    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // 设置realm.
        securityManager.setRealm(myShiroRealm());
        // 自定义缓存实现 使用redis
        securityManager.setCacheManager(cacheManager());
        // 自定义session管理 使用redis
        securityManager.setSessionManager(sessionManager());
        return securityManager;
    }
    
    /**
     * @类名: ShiroConfig.java 
     * @时间: 2021年12月6日 下午4:38:40 
     * @作者: 秦二少
     * @return
     * @描述: 凭证匹配器(密码校验交给Shiro的SimpleAuthenticationInfo进行处理)
     */
    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher(){
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        hashedCredentialsMatcher.setHashAlgorithmName("md5");//散列算法:这里使用MD5算法;
        hashedCredentialsMatcher.setHashIterations(1);//散列的次数,比如散列两次,相当于 md5(md5(""));
        return hashedCredentialsMatcher;
    }
    
    /**
     * @类名: ShiroConfig.java 
     * @时间: 2021年12月6日 下午4:39:33 
     * @作者: 秦二少
     * @param securityManager
     * @return
     * @描述: 权限管理
     */
	@Bean
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
		ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
		shiroFilterFactoryBean.setSecurityManager(securityManager);
		// 没有登陆的用户只能访问登陆页面
		shiroFilterFactoryBean.setLoginUrl("/login");
		// 登录成功后要跳转的链接
		shiroFilterFactoryBean.setSuccessUrl("/index");
		//未授权跳转链接
		shiroFilterFactoryBean.setUnauthorizedUrl("/error/toUnauthorized");
		//自定义拦截器
		Map<String, Filter> filtersMap = new LinkedHashMap<String, Filter>();
		//限制同一帐号同时在线的个数。
		filtersMap.put("kickout", kickoutSessionControlFilter());
		shiroFilterFactoryBean.setFilters(filtersMap);
		// 权限控制map.
		Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
		// <!-- authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问-->
		//filterChainDefinitionMap.put("/index/**", "authc");//主页
		
		filterChainDefinitionMap.put("/company/register/**", "anon");//跳转到企业注册页
		filterChainDefinitionMap.put("/company/addCompany/**", "anon");//企业注册
		
		filterChainDefinitionMap.put("/register/**", "anon");//跳转到新供应商注册页
		filterChainDefinitionMap.put("/login/**", "anon");//跳转到登录页
		filterChainDefinitionMap.put("/user/LoginValidate/**", "anon");//登录验证
		filterChainDefinitionMap.put("/drawRandomJPG/**", "anon");//登录验证码
		filterChainDefinitionMap.put("/getSMSCode/**", "anon");//短信验证码
		filterChainDefinitionMap.put("/error/**", "anon");//错误
		filterChainDefinitionMap.put("/script/**", "anon");//静态文件
		filterChainDefinitionMap.put("/api/**", "anon");//接口
		//主要这行代码必须放在所有权限设置的最后,不然会导致所有 url 都被拦截 剩余的都需要认证
		//filterChainDefinitionMap.put("/**", "authc");
		filterChainDefinitionMap.put("/**", "kickout,authc");//必须要这样写才能进入 限制同一个帐号同时在线的个数 的拦截器中。
        //加载权限
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }

    /**
     * cacheManager 缓存 redis实现
     * 使用的是shiro-redis开源插件
     * @return
     */
    public RedisCacheManager cacheManager() {
        RedisCacheManager redisCacheManager = new RedisCacheManager();
        redisCacheManager.setRedisManager(redisManager());
        // 配置缓存的话要求放在session里面的实体类必须有个id标识
        redisCacheManager.setPrincipalIdFieldName("id");
        return redisCacheManager;
    }

    /**
     * 配置shiro redisManager
     * 使用的是shiro-redis开源插件
     * @return
     */
    public RedisManager redisManager() {
        RedisManager redisManager = new RedisManager();
        //在比较旧的版本是host和port分开写,新的版本需要把host和port拼接起来,
        redisManager.setHost("127.0.0.1");//127.0.0.1
        redisManager.setPort(6379);//6379
        redisManager.setTimeout(3000);//3000
        //redisManager.setExpire(2160000);// 配置缓存过期时间
        redisManager.setDatabase(2);//2
        //redisManager.setPassword(password);
        return redisManager;
    }

    /**
     * Session Manager
     * 使用的是shiro-redis开源插件
     */
    @Bean
    public DefaultWebSessionManager sessionManager() {
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        sessionManager.setSessionDAO(redisSessionDAO());
        return sessionManager;
    }

    /**
     * RedisSessionDAO shiro sessionDao层的实现 通过redis
     * 使用的是shiro-redis开源插件
     */
    @Bean
    public RedisSessionDAO redisSessionDAO() {
        RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
        redisSessionDAO.setExpire(21600000);
        redisSessionDAO.setRedisManager(redisManager());
        return redisSessionDAO;
    }

    /**
     * 限制同一账号登录同时登录人数控制
     * 注意:这里不能加Bean注解,因为那个拦截器里面已经有了
     */
    public KickoutSessionControlFilter kickoutSessionControlFilter() {
        KickoutSessionControlFilter kickoutSessionControlFilter = new KickoutSessionControlFilter();
        kickoutSessionControlFilter.setCacheManager(cacheManager());
        kickoutSessionControlFilter.setSessionManager(sessionManager());
        kickoutSessionControlFilter.setKickoutAfter(false);
        kickoutSessionControlFilter.setMaxSession(1);
        kickoutSessionControlFilter.setKickoutUrl("/login");
        return kickoutSessionControlFilter;
    }


    /***
     * 授权所用配置
     */
    @Bean
    public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
        return defaultAdvisorAutoProxyCreator;
    }

    /**
     * @类名: ShiroConfig.java 
     * @时间: 2021年12月6日 下午4:40:03 
     * @作者: 秦二少
     * @param securityManager
     * @return
     * @描述: 开启shiro aop注解支持.
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }

    /**
     * Shiro生命周期处理器
     *
     */
    @Bean
    public LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }
    
}

其他的地方都差不多,重点需要注意的就是 shiroFilter 这个方法,
filterChainDefinitionMap.put(“/**”, “kickout,authc”); 一定要这样加,我就是加错了,这个限制始终不生效,搞了很多久才发现是这里的问题。

其他的都是常见的配置了,如果有不明白的可以找我。大家一起学习!

完毕!

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

springboot2+shiro+redis限制同一账号同时在线人数 的相关文章

  • 有没有办法用Lettuce自动发现Redis集群中新的集群节点IP

    我有一个Redis集群 3主3从 运行在一个库伯内斯簇 该集群通过Kubernetes 服务 Kube 服务 我将我的应用程序服务器连接到 Redis 集群 使用Kube 服务作为 URI 通过 Redis 的 Lettuce java 客
  • 如何使redis中的“HSET”子键“过期”?

    我需要使 Redis 哈希中所有超过 1 个月的密钥过期 这不可能 https github com antirez redis issues 167 issuecomment 2559040 为了保持 Redis 简单 https git
  • StackExchange.Redis的正确使用方法

    这个想法是使用更少的连接和更好的性能 连接会随时过期吗 对于另一个问题 redis GetDatabase 打开新连接 private static ConnectionMultiplexer redis private static ID
  • 在 Redis 上为 Django 和 Express.js 应用程序共享会话存储

    我想创建一个包含一些登录用户的 Django 应用程序 另一方面 由于我想要一些实时功能 所以我想使用 Express js 应用程序 现在的问题是 我不希望身份不明的用户访问 Express js 应用程序的日期 因此 我必须在 Expr
  • Scala 使用的 Redis 客户端库建议

    我正在计划使用 Scala 中的 Redis 实例进行一些工作 并正在寻找有关使用哪些客户端库的建议 理想情况下 如果存在一个好的库 我希望有一个为 Scala 而不是 Java 设计的库 但如果现在这是更好的方法 那么仅使用 Java 客
  • Spring Redis删除不删除key

    我正在尝试删除一个 Redis 键 但由于某种原因它没有删除 但也没有抛出异常 这是我要删除的代码 import com example service CustomerService import com example model Cu
  • 何时从容器管理的安全性转向 Apache Shiro、Spring Security 等替代方案?

    我正在尝试保护使用 JSF2 0 构建的应用程序的安全 我很困惑人们什么时候会选择使用 Shiro Spring Security 或 owasp 的 esapi 等安全替代方案 而放弃容器管理的安全性 看过一些相关问题 https sta
  • Spring Data Redis 覆盖默认序列化器

    我正在尝试创建一个RedisTemplatebean 将具有更新的值序列化器来序列化对象JSONredis 中的格式 Configuration class RedisConfig Bean name redisTemplate Prima
  • ServiceStack.Redis:无法连接:sPort:

    我经常得到 ServiceStack Redis 无法连接 sPort 0 或 ServiceStack Redis 无法连接 sPort 50071 或其他端口号 当我们的网站比较繁忙时 就会出现这种情况 Redis 本身看起来很好 CP
  • 没有适用于机器人的 Laravel 会话

    我在大型 Laravel 项目和 Redis 存储方面遇到问题 我们将会话存储在 Redis 中 我们已经有 28GB 的 RAM 然而 它的运行速度仍然相对较快 达到了极限 因为我们有来自搜索引擎机器人的大量点击 每天超过 250 000
  • Web API 缓存 - 如何使用分布式缓存实现失效

    我有一个 API 目前不使用任何缓存 我确实有一个正在使用的中间件 它可以生成缓存标头 Cache Control Expires ETag Last Modified 使用https github com KevinDockx HttpC
  • 集合成员的 TTL

    Redis 是否可以不为特定键而是为集合的成员设置 TTL 生存时间 我正在使用 Redis 文档提出的标签结构 数据是简单的键值对 标签是包含与每个标签对应的键的集合 例如 gt SETEX id id 1 100 Lorem ipsum
  • Redis 是否使用用户名进行身份验证?

    我已经在我的环境中设置了Redis 并且只看到了通过密码授权的部分 有没有办法也设置用户名 还是只能通过密码验证 Redis 6 上有 ACL 这些都有一个用户名 查看https redis io topics acl https redi
  • 为什么我们需要 Redis 来运行 CKAN?

    我想知道为什么我们需要 Redis 服务器来运行 CKAN 如果需要 为什么 我如何使用 CKAN 配置它 附注 我正在 RHEL7 中运行我的 ckan 实例 Update Redis 已成为一项要求从CKAN 2 7开始 https d
  • 使用通配符查找键

    我已经使用分号保存了数据 redis gt keys party 1 party congress president 2 party bjp president 3 party bjp 4 party sena 是否有任何命令可以列出所有
  • 如何高效地将数十亿数据插入Redis?

    我有大约 20 亿个键值对 我想将它们有效地加载到 Redis 中 我目前正在使用 Python 并使用 Pipe 如redis py https redis py readthedocs io en latest redis Redis
  • 为什么Redis SET性能优于GET?

    根据Redis基准 http redis io topics benchmarkss Redis 可以执行 100 000 SET 操作 秒和 80 000 GET 操作 秒 Redis 是一种内存数据库 这似乎令人惊讶 因为通常人们会认为
  • Spring-boot中将redis-cache反序列化为对象的问题

    我在 Client 类中使用 JsonNode 来处理 MySQL 8 数据库中 JSON 类型的字段 即使对于 API 请求 它也能很好地工作 但是当我使用 Redis 启用缓存 我确实需要它 时 我注意到 Redis 无法序列化 Jso
  • 如何在Redis中存储聚合目录树搜索结果

    我有一个很大的产品目录树 目前包含约 36000 个类别和约 100 万个产品 即叶子 它的结构如下 最大深度为 5 Cat1 Cat11 Cat111 Cat1111 Product1 Cat1112 Product1 Cat1113 P
  • 使用 MongoDB 作为我们的主数据库,我应该使用单独的图数据库来实现实体之间的关系吗?

    我们目前正在为一家专业公司内部实施类似 CRM 的解决方案 由于存储信息的性质以及信息的不同值和键 我们决定使用文档存储数据库 因为它完全适合目的 在本例中我们选择 MongoDB 作为此 CRM 解决方案的一部分 我们希望存储实体之间的关

随机推荐