SSM整合Shiro(四)

同一个账号登陆人数控制

实现思路

为每一个账号创建一个队列,用来保存登录该账号的sessionId。以账号ID(也就是userId)为key,对应的队列为value的形式存入Redis。每当用户登录时执行以下操作:

  • 根据userId,从Redis中取得队列;
  • 判断当前的session,如果sessionId不在队列中,而且该session不是被强制下线的,将sessionId放入队列;
  • 判断队列大小是否大于设置的最大登录人数,如果大于最大登录人数,从队列中删除该sessionId,并且设置session为强制下线状态;
  • 根据session是否是被强制下线的进行强制下线操作;

创建登录人数控制过滤器

package com.demo.filter;

import java.io.Serializable;
import java.util.concurrent.ConcurrentLinkedDeque;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;

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 org.springframework.beans.factory.InitializingBean;

import com.demo.model.LearnShiroUser;

/**
 * 
 * 控制同一账号登陆人数<br>
 * <br>
 * -------------------------------------<br>
 * 创建人员: ToBeNumberTwo<br>
 * 创建时间: 2020年5月19日 下午7:34:44<br>
 * -------------------------------------<br>
 * 修改人员: ToBeNumberTwo<br>
 * 修改时间: 2020年5月19日 下午7:34:44<br>
 * -------------------------------------<br>
 */
public class ShiroMyUserLoginCountFilter extends AccessControlFilter implements InitializingBean {

    private String forceOfflineUrl;
    private boolean forceOfflineHeadFlg;
    private int maxUserCount;
    private SessionManager sessionManager;
    private String loginUserDequeCacheName;
    private CacheManager cacheManager;
    private Cache<String, ConcurrentLinkedDeque<Serializable>> loginUserDequeCache;

    public void setForceOfflineUrl(String forceOfflineUrl) {
        this.forceOfflineUrl = forceOfflineUrl;
    }

    public void setForceOfflineHeadFlg(boolean forceOfflineHeadFlg) {
        this.forceOfflineHeadFlg = forceOfflineHeadFlg;
    }

    public void setMaxUserCount(int maxUserCount) {
        this.maxUserCount = maxUserCount;
    }

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

    public void setCacheManager(CacheManager cacheManager) {
        this.cacheManager = cacheManager;
    }

    public void setLoginUserDequeCacheName(String loginUserDequeCacheName) {
        this.loginUserDequeCacheName = loginUserDequeCacheName;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        this.loginUserDequeCache = this.cacheManager.getCache(this.loginUserDequeCacheName);
    }

    @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;
        }

        // 从subject中获取session和user信息
        Session session = subject.getSession();
        LearnShiroUser user = (LearnShiroUser) subject.getPrincipal();
        String userId = "" + user.getUserId();
        Serializable sessionId = session.getId();
        // 从缓存中获取用户队列
        ConcurrentLinkedDeque<Serializable> loginUserDeque = loginUserDequeCache.get(userId);

        // 初始化用户的队列
        if (loginUserDeque == null) {
            loginUserDeque = new ConcurrentLinkedDeque<Serializable>();
        }

        // 如果队列里没有此sessionId,且用户没有被强制下线,放入队列
        if (!loginUserDeque.contains(sessionId) && session.getAttribute("hasRemoved") == null) {
            loginUserDeque.push(sessionId);
        }

        while (loginUserDeque.size() > maxUserCount) {
            // 被强制下线的sessionId
            Serializable removedSessionId = null;

            if (forceOfflineHeadFlg) {
                // 强制下线先登录用户
                removedSessionId = loginUserDeque.pollLast();
            } else {
                // 强制下线后登录用户
                removedSessionId = loginUserDeque.pollFirst();
            }

            try {
                Session removedSession = sessionManager.getSession(new DefaultSessionKey(removedSessionId));
                if (removedSession != null) {
                    // 设置被强制下线的Session
                    removedSession.setAttribute("hasRemoved", true);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        // 刷新缓存中队列的值
        loginUserDequeCache.put(userId, loginUserDeque);

        // 判断当前用户session是强制下线状态
        if (session.getAttribute("hasRemoved") != null) {
            try {
                subject.logout();
            } catch (Exception e) {
            }
            WebUtils.issueRedirect(request, response, forceOfflineUrl);
            return false;
        }

        return true;
    }
}

创建自定义ShiroSession监听器,当Session失效时从对应的队列中删除sessionId

package com.demo.listener;

import java.io.Serializable;
import java.util.concurrent.ConcurrentLinkedDeque;

import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheManager;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.SessionListener;
import org.springframework.beans.factory.InitializingBean;

import com.demo.model.LearnShiroUser;

/**
 * 
 * 自定义ShiroSession监听器,当Session失效时从对应的队列中删除sessionId<br>
 * <br>
 * -------------------------------------<br>
 * 创建人员: ToBeNumberTwo<br>
 * 创建时间: 2020年5月19日 下午7:30:47<br>
 * -------------------------------------<br>
 * 修改人员: ToBeNumberTwo<br>
 * 修改时间: 2020年5月19日 下午7:30:47<br>
 * -------------------------------------<br>
 */
public class ShiroMyForceOfflineSessionListener implements SessionListener, InitializingBean {

    /** 登陆用户session队列缓存 */
    private Cache<String, ConcurrentLinkedDeque<Serializable>> loginUserDequeCache;
    private String loginUserDequeCacheName;
    private CacheManager cacheManager;

    @Override
    public void afterPropertiesSet() throws Exception {
        this.loginUserDequeCache = this.cacheManager.getCache(this.loginUserDequeCacheName);
    }

    /**
     * session创建时触发
     * 
     * @see org.apache.shiro.session.SessionListener#onStart(org.apache.shiro.session.Session)
     * @param session
     */
    @Override
    public void onStart(Session session) {
    }

    /**
     * session停用时触发(例如注销登陆时)
     * 
     * @see org.apache.shiro.session.SessionListener#onStop(org.apache.shiro.session.Session)
     * @param session
     */
    @Override
    public void onStop(Session session) {
        removeLoginUserDequeCache(session);
    }

    /**
     * session过期时触发
     * 
     * @see org.apache.shiro.session.SessionListener#onExpiration(org.apache.shiro.session.Session)
     * @param session
     */
    @Override
    public void onExpiration(Session session) {
        removeLoginUserDequeCache(session);
    }

    /**
     * 清除缓存中该session对应的用户队列中的值
     * 
     * @param session
     */
    private void removeLoginUserDequeCache(Session session) {
        Serializable sessionId = session.getId();
        LearnShiroUser user = session.getAttribute("user") != null ? (LearnShiroUser) session.getAttribute("user") : null;
        if (user != null) {
            ConcurrentLinkedDeque<Serializable> loginUserDeque = loginUserDequeCache.get("" + user.getUserId());
            if (loginUserDeque != null) {
                loginUserDeque.remove(sessionId);
                // 刷新缓存
                loginUserDequeCache.put("" + user.getUserId(), loginUserDeque);
            }
        }
    }

    /**
     * loginUserDequeCacheNameを取得します。
     * 
     * @return loginUserDequeCacheName
     */
    public String getLoginUserDequeCacheName() {
        return loginUserDequeCacheName;
    }

    /**
     * loginUserDequeCacheNameを設定します。
     * 
     * @param loginUserDequeCacheName loginUserDequeCacheName
     */
    public void setLoginUserDequeCacheName(String loginUserDequeCacheName) {
        this.loginUserDequeCacheName = loginUserDequeCacheName;
    }

    /**
     * cacheManagerを取得します。
     * 
     * @return cacheManager
     */
    public CacheManager getCacheManager() {
        return cacheManager;
    }

    /**
     * cacheManagerを設定します。
     * 
     * @param cacheManager cacheManager
     */
    public void setCacheManager(CacheManager cacheManager) {
        this.cacheManager = cacheManager;
    }
}

修改application-shiro.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

	<!-- 注入自定义Realm对象(用户IDRealm) -->
	<bean id="userIdRealm" class="com.demo.realm.UserIdRealm">
		<!-- 开启缓存 -->
		<property name="cachingEnabled" value="true"></property>
		<!-- 开启认证缓存 -->
		<property name="authenticationCachingEnabled" value="true"></property>
		<!-- 认证缓存名称 -->
		<property name="authenticationCacheName" value="authentication:UserIdRealm"></property>
		<!-- 开启授权缓存 -->
		<property name="authorizationCachingEnabled" value="true"></property>
		<!-- 授权缓存名称 -->
		<property name="authorizationCacheName" value="authorization:UserIdRealm"></property>
	</bean>

	<!-- 注入Cookie对象 -->
	<bean id="rememberMeCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
		<!-- Cookie名 必须赋值且value固定 -->
		<constructor-arg value="rememberMe"></constructor-arg>
		<!-- 设置cookie有效时间单位:秒。 -->
		<!-- <property name="maxAge" value="604800"></property> -->
		<!-- 使用spring el表达式来计算 -->
		<property name="maxAge" value="#{7*24*60*60}"></property>
		<!-- 只有HTTP请求才保存cookie -->
		<property name="httpOnly" value="true"></property>
	</bean>

	<!-- 注入Cookie管理器 -->
	<bean id="rememberMeCookieManager" class="org.apache.shiro.web.mgt.CookieRememberMeManager">
		<property name="cookie" ref="rememberMeCookie"></property>
	</bean>

	<!-- 注入Redis管理器 -->
	<bean id="redisManager" class="org.crazycake.shiro.RedisManager">
		<!-- 连接Redis的ip和端口号 -->
		<property name="host" value="xxxxxx:xxxx"></property>
		<!-- 如果Redis设置了登陆密码就需要赋值 -->
		<property name="password" value="xxxx"></property>
		<!-- 连接Redis的哪个库 -->
		<property name="database" value="2"></property>
		<!-- 连接超时时间 -->
		<property name="timeout" value="2000"></property>
	</bean>

	<!-- 注入缓存管理器 -->
	<bean id="cacheManager" class="org.crazycake.shiro.RedisCacheManager">
		<property name="redisManager" ref="redisManager"></property>
		<!-- 存放登陆认证和授权信息的缓存Key的前缀 默认值为shiro:cache: -->
		<property name="keyPrefix" value="learnshiroredisSSM:cache:"></property>
		<!-- 认证时 创建SimpleAuthenticationInfo对象的时,给它构造方法赋的第一个参数(user),对应的类中要有这个属性而且还有get方法。默认值是id -->
		<property name="principalIdFieldName" value="userId"></property>
		<!-- 有效时间 -->
		<property name="expire" value="1800"></property>
	</bean>

	<!-- 注入Shiro的redisSessionDAO -->
	<bean id="redisSessionDAO" class="org.crazycake.shiro.RedisSessionDAO">
		<property name="redisManager" ref="redisManager"></property>
		<!-- 存放Session的缓存的Key的前缀 默认值为shiro:session: -->
		<property name="keyPrefix" value="learnshiroredisSSM:session:"></property>
		<!-- 有效时间 -->
		<property name="expire" value="1800"></property>
	</bean>

	<!-- 注入自定义SessionListener -->
	<bean id="shiroMyForceOfflineSessionListener" class="com.demo.listener.ShiroMyForceOfflineSessionListener">
		<property name="cacheManager" ref="cacheManager"></property>
		<property name="loginUserDequeCacheName" value="loginUserDequeCache"></property>
	</bean>

	<!-- 注入Shiro的Session管理器 -->
	<bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
		<property name="sessionDAO" ref="redisSessionDAO"></property>
		<!-- 设置自定义监听器 -->
		<property name="sessionListeners">
			<list>
				<ref bean="shiroMyForceOfflineSessionListener" />
			</list>
		</property>
	</bean>

	<!-- 注入安全管理器 -->
	<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
		<property name="rememberMeManager" ref="rememberMeCookieManager"></property>
		<property name="sessionManager" ref="sessionManager"></property>
		<property name="cacheManager" ref="cacheManager"></property>
		<property name="realm" ref="userIdRealm"></property>
	</bean>

	<!-- 配置授权属性 -->
	<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
		<property name="securityManager" ref="securityManager" />
	</bean>

	<bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
		<property name="staticMethod" value="org.apache.shiro.SecurityUtils.setSecurityManager" />
		<property name="arguments" ref="securityManager" />
	</bean>

	<!-- 注入自定义过滤器 -->
	<!-- 记住我功能使用 -->
	<bean id="shiroMyRememberMeFilter" class="com.demo.filter.ShiroMyRememberMeFilter"></bean>
	<!-- 控制同一账号登录人数使用 -->
	<bean id="shiroMyUserLoginCountFilter" class="com.demo.filter.ShiroMyUserLoginCountFilter">
		<property name="forceOfflineUrl" value="/login/hasRemoved"></property>
		<property name="forceOfflineHeadFlg" value="true"></property>
		<property name="maxUserCount" value="1"></property>
		<property name="sessionManager" ref="sessionManager"></property>
		<property name="cacheManager" ref="cacheManager"></property>
		<property name="loginUserDequeCacheName" value="loginUserDequeCache"></property>
	</bean>

	<!-- 配置shiro的过滤器链 -->
	<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
		<!-- 安全管理器 -->
		<property name="securityManager" ref="securityManager" />
		<!-- 未认证时跳转 -->
		<property name="loginUrl" value="/login/init" />
		<!-- 未授权时跳转 -->
		<property name="unauthorizedUrl" value="/unauthorized.jsp" />
		<!-- 将自定义过滤器添加进来 -->
		<property name="filters">
			<map>
				<entry key="shiroMyRememberMeFilter" value-ref="shiroMyRememberMeFilter"></entry>
				<entry key="shiroMyUserLoginCountFilter" value-ref="shiroMyUserLoginCountFilter"></entry>
			</map>
		</property>

		<!-- 配置放行规则 -->
		<property name="filterChainDefinitions">
			<value>
				#不需要认证
				/login/init/** = anon
				/login/login/** = anon
				/login/logout/** = anon
				/login/hasRemoved/** = anon
				/resources/** = anon
				#权限授权拦截
				/home/superAdminByFilter=shiroMyUserLoginCountFilter,perms["superAdmin"]

				#记住我功能
				/**=shiroMyUserLoginCountFilter,shiroMyRememberMeFilter,user
				#需要认证
				#使用记住我功能时,不能用/**=authc改为以下方式
				/*=shiroMyUserLoginCountFilter,authc
				/*/*=shiroMyUserLoginCountFilter,authc
			</value>
		</property>
	</bean>
</beans>

效果

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

补充

上面控制登录人数的实现方式有一个bug,当同一账号最大登录人数大于1时,不使用记住登录功能时一切正常。当有一个人登录时选了记住登录,然后不注销登录,直接关闭浏览器。在他的session未失效之前,重新用浏览器直接访问主页时,会新生成一个session,这就多占用了一个登录名额…
目前想到的两个都不是很完美的解决方案:
一个就是“js监听到关闭浏览器时,通知后台去销毁session。但是js监听浏览器事件兼容性一直没解决”。
另外一个就是“在用户认证的时候,也就是UserIdRealm的doGetAuthenticationInfo方法中,把当前的sessionId存到user对象里。当用户关闭浏览器后再重新用浏览器直接访问主页时,user中的sessionId和新产生的sessionId是不一样的,而队列中的sessionId和user中的sessionId相同。在ShiroMyUserLoginCountFilter 进行处理时判断,如果是通过记住登录功能进行登录的就把队列中的sessionId替换成新产生的sessionId。这样就不会有多占名额的问题了”。这样改了之后认证时就不能用缓存。

能力不足,还请各位手下留情…

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值