需求:实现同一个账号同一个时刻只能在一个IP浏览器登录,后者把前者挤掉线。
实现:通过shiro的session管理机制进行实现。
我的理解:shrio有一套sessionmanager的管理器,用来管理登录用户的session。不同的用户每次登录都会生成一个sessionid,保存在shrio框架中。我只需要实现根据登录用户的账号,判断正在登录的用户session是否已经存在于当前活跃的session列表中,如果存在的话,把那个session给删除,就可以成功实现将前一个登录的用户挤掉了。
这一段的逻辑代码是:
在自定义的ShiroJdbcRealm类中的doGetAuthenticationInfo--认证回调函数中加上:
long loginOnTime = new Date().getTime();
String tempSessionId = SecurityUtils.getSubject().getSession().getId().toString();//获取当前正在登录的用户的sessionID(已经登录了的,这是回调函数)
logger.info("正在登陆---登陆用户{}",tempSessionId);
AuthUserDetails userSession = null;
int count = 0;
if(authAccount != null){
//获取在线的session,获取当前所有活跃的用户;包括正在登陆的这个用户
Collection<Session> sessionCollection = sessionDAO.getActiveSessions();
for (Session session : sessionCollection){
// 获取simpleAuthenticationInfo的第一个参数的值
if(session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY) != null) {
//根据session build出一个subject
Subject subject = new Subject.Builder().session(session).buildSubject();
//循环遍历拿到已经登录的对象
userSession = (AuthUserDetails) subject.getPrincipal();
logger.info("缓存用户{}",userSession);
//判断已经登录的对象的code和现在正在登陆的code是否一致 ,这里的code用的是登录的账号
if (authAccount.getAuthUid().equals(userSession.getAuthUid())) {
//同一个账户登录的时候,除了当前正在登录的用户,其他的session都要移除。
if (session.getId()!=tempSessionId) {
sessionDAO.delete(session);//从活跃的session列表中移除同一个账号的其他session,除了当前正在登录的session
count++;
}
}
}
}
}
System.out.println("---------------------------"+count);
由于每一个http请求都会生成一个sessionID,所以实际上会移除很多session,但是都是这个账号的,所以无所谓了。
其中sessionDAO.getActiveSessions();这一行是会获取到当前活跃的所有的session(也就是没有过期的包括当前正在登录的用户的session)
shiro自己的配置文件讲解:
spring-shiro.xml,这里面就是用来定义bean,然后加载到其他定义bean中当前属性值进行使用,这样的话就不用自己实例化了,在代码里可以直接使用。
<?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-4.0.xsd"
default-lazy-init="true">
<description>Shiro安全配置</description>
<!-- shiro cache using Redis
<bean id="shiroRedisManager" class="org.crazycake.shiro.RedisManager">
<property name="host" value="${redis.host}" />
<property name="port" value="${redis.port}" />
<property name="expire" value="1800" />
</bean>
<bean id="shiroCacheManager" class="org.crazycake.shiro.RedisCacheManager">
<property name="redisManager" ref="shiroRedisManager" />
</bean>
-->
<!--定义sessiondao,用来查询数据库中的账号信息,根据登录账号,自己实现-->
<bean id="sessionDao" class="org.apache.shiro.session.mgt.eis.MemorySessionDAO"/>
<bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
<!-- session超时时间 ,这里是半个小时 ref="sessionDao"标识把sessiondao注入到这里-->
<property name="globalSessionTimeout" value="1800000"/>
<property name="sessionDAO" ref="sessionDao"/>
<!-- <property name="sessionValidationScheduler" ref="sessionValidationScheduler"/> -->
<!-- 删除失效的session -->
<property name="deleteInvalidSessions" value="true"/>
<!-- 是否开启会话验证器,默认是开启的 -->
<property name="sessionValidationSchedulerEnabled" value="true"/>
<!-- 相隔多久检查一次session的有效性 半个小时,每半个显示去检查一下session是否过期,如果没半个小时无操作,也会认为是失效的session -->
<property name="sessionValidationInterval" value="1800000"/>
</bean>
<!-- shiro cache using EhCache 设置depends-on="cacheManager",确保共享模式下优先加载Spring CacheManager -->
<bean id="shiroCacheManager" class="cc.rengu.ecp.platform.core.security.SharedEhCacheManager" depends-on="cacheManager">
<property name="cacheManagerConfigFile" value="classpath:ehcache-config.xml" />
<property name="shared" value="true" />
</bean>
<!--自定义realm-->
<bean id="shiroJdbcRealm" class="cc.rengu.ecp.platform.core.security.ShiroJdbcRealm">
<property name="passwordService" ref="passwordService" />
<property name="userService" ref="userService" />
<property name="sessionDAO" ref="sessionDao" />
</bean>
<bean id="authenticator" class="org.apache.shiro.authc.pam.ModularRealmAuthenticator">
<property name="authenticationStrategy">
<bean class="org.apache.shiro.authc.pam.FirstSuccessfulStrategy" />
</property>
</bean>
<!-- Shiro's main business-tier object for web-enabled applications 这里是shiro配置的核心,将session管理器、缓存管理器、注入到里面-->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="authenticator" ref="authenticator" />
<property name="realms">
<list>
<ref bean="shiroJdbcRealm" />
</list>
</property>
<property name="cacheManager" ref="shiroCacheManager" />
<property name="sessionManager" ref="sessionManager"></property>
</bean>
<!-- spring-shiro.xml加入以下BEAN -->
<!-- 定义一个拦截器,限制同一用户登录 -->
<bean id="kickoutSessionControlFilter" class="cc.rengu.ecp.platform.core.security.KickoutSessionControlFilter">
<property name="cacheManager" ref="shiroCacheManager"/>
<property name="sessionDAO" ref="sessionDao" />
<property name="securityManager" ref="securityManager"/>
<property name="kickoutUrl" value="/admin/login"/>
</bean>
<bean id="anyRolesAuthorizationFilter" class="cc.rengu.ecp.platform.core.security.AnyRolesAuthorizationFilter" />
<bean id="wwwJcaptchaFormAuthenticationFilter" class="cc.rengu.ecp.platform.core.security.JcaptchaFormAuthenticationFilter">
<property name="loginUrl" value="/w/login" />
<property name="successUrl" value="/w" />
<property name="userService" ref="userService" />
</bean>
<bean id="mobileJcaptchaFormAuthenticationFilter" class="cc.rengu.ecp.platform.core.security.JcaptchaFormAuthenticationFilter">
<property name="loginUrl" value="/m/login" />
<property name="successUrl" value="/m" />
<property name="userService" ref="userService" />
</bean>
<bean id="adminJcaptchaFormAuthenticationFilter" class="cc.rengu.ecp.platform.core.security.JcaptchaFormAuthenticationFilter">
<property name="loginUrl" value="/admin/login" />
<property name="successUrl" value="/admin" />
<property name="userService" ref="userService" />
</bean>
<bean id="appFormAuthenticationFilter" class="cc.rengu.ecp.platform.core.security.JcaptchaFormAuthenticationFilter">
<property name="loginUrl" value="/app/login" />
<property name="userService" ref="userService" />
</bean>
<bean id="appBasicAuthenticationFilter" class="cc.rengu.ecp.platform.core.security.AppBasicAuthenticationFilter">
<property name="appKeySecrets">
<props>
<prop key="auth_app_android">${auth_app_android}</prop>
<prop key="auth_app_ios">${auth_app_ios}</prop>
<prop key="auth_app_weixin">${auth_app_weixin}</prop>
<prop key="auth_api_jenkins">${auth_api_jenkins}</prop>
</props>
</property>
</bean>
<bean id="bearerTokenAuthenticatingFilter" class="cc.rengu.ecp.platform.core.security.BearerTokenAuthenticatingFilter">
</bean>
<bean id="wwwLogoutFilter" class="org.apache.shiro.web.filter.authc.LogoutFilter">
<property name="redirectUrl" value="/w" />
</bean>
<bean id="mobileLogoutFilter" class="org.apache.shiro.web.filter.authc.LogoutFilter">
<property name="redirectUrl" value="/m" />
</bean>
<bean id="adminLogoutFilter" class="org.apache.shiro.web.filter.authc.LogoutFilter">
<property name="redirectUrl" value="/admin" />
</bean>
<bean id="appLogoutFilter" class="cc.rengu.ecp.platform.core.security.AppLogoutFilter">
<property name="userService" ref="userService" />
</bean>
<!-- Shiro Filter ,这里是shiro的拦截器链,不同的角色对应不同的拦截器-->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="filters">
<map>
<entry key="anyRoles" value-ref="anyRolesAuthorizationFilter" />
<entry key="authcWww" value-ref="wwwJcaptchaFormAuthenticationFilter" />
<entry key="authcMobile" value-ref="mobileJcaptchaFormAuthenticationFilter" />
<entry key="authcA" value-ref="adminJcaptchaFormAuthenticationFilter" />
<entry key="authcBearerToken" value-ref="bearerTokenAuthenticatingFilter" />
<entry key="authcAppSecret" value-ref="appBasicAuthenticationFilter" />
<entry key="authcAppForm" value-ref="appFormAuthenticationFilter" />
<entry key="logoutWww" value-ref="wwwLogoutFilter" />
<entry key="logoutMobile" value-ref="mobileLogoutFilter" />
<entry key="logoutAdmin" value-ref="adminLogoutFilter" />
<entry key="logoutApp" value-ref="appLogoutFilter" />
<entry key="kickout" value-ref="kickoutSessionControlFilter" />
</map>
</property>
<property name="securityManager" ref="securityManager" />
<!--
anon org.apache.shiro.web.filter.authc.AnonymousFilter
authc org.apache.shiro.web.filter.authc.FormAuthenticationFilter
authcBasic org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter
logout org.apache.shiro.web.filter.authc.LogoutFilter
noSessionCreation org.apache.shiro.web.filter.session.NoSessionCreationFilter
perms org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter
port org.apache.shiro.web.filter.authz.PortFilter
rest org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter
roles org.apache.shiro.web.filter.authz.RolesAuthorizationFilter
ssl org.apache.shiro.web.filter.authz.SslFilter
user org.apache.shiro.web.filter.authc.UserFilter
不同的url对应不同的拦截器,实现对不同的url进行不同的拦截处理,这是拦截器链,真正的拦截器执行顺序就是根据这个地址进行拦截
-->
<property name="filterChainDefinitions">
<value>
/assets/** = anon
/system/** = anon
/widgets/** = anon
/w/login = authcWww
/w/logout = logoutWww
/w* = anon
/m/login = authcMobile
/m/logout = logoutMobile
/m/** = authcBearerToken,anon
/admin/login = authcA
/admin/logout = logoutAdmin
/admin** = kickout
/admin/* = kickout
/admin/*/*=kickout
/admin** = authcA,anyRoles[ROLE_MGMT_USER]
/druid/** = authcA,anyRoles[ROLE_SUPER_USER]
/app/login = authcAppForm
/app/logout = logoutApp
/app/** = authcAppSecret,authcBearerToken
/api/** = authcAppSecret
/appdvlp/** = anon
</value>
</property>
</bean>
</beans>
这个拦截URL配置是有规则的
过滤器角色,可以自定义过滤器key:
- anon 不需要认证
- authc 需要认证
- user 验证通过或RememberMe登录的都可以
URL匹配说明:
- /admin?=authc 表示可以请求以admin开头的字符串,如xxx/adminfefe,但无法匹配多个,即xxx/admindf/admin是不行的
- /admin*=authc 表示可以匹配零个或者多个字符,如/admin,/admin1,/admin123,但是不能匹配/admin/abc这种
- /admin/**=authc 表示可以匹配零个或者多个路径,如/admin,/admin/ad/adfdf等
注意:
/login=anon 不会对http://localhost:8080/login/起效果
/login=anon 和 /login=anon/ 不一样
相同url但定义在不同的行,后面覆盖前面
如
/usr/login.do=test3 /usr/login.do=test1,test2 不会执行test3的filter
同一个url可以匹配不同的规则,但只执行首行,请求url过来的时候,先匹配到哪个过滤器,就执行哪个过滤器,后面的就不会执行了。 /usr/* =test1,test2 /usr/login.do=test3 url = /usr/login.do请求来了,不会执行test3,因为已经匹配了/usr/* =test1,test2 要解答该问题,需要知道每个url的FilterChain是如何获取的,每个url都有自己的filterchain。
本例中
/admin/login = authcA
/admin/logout = logoutAdmin
/admin** = kickout
/admin/* = kickout
/admin/*/*=kickout
/admin** = authcA,anyRoles[ROLE_MGMT_USER]
/druid/** = authcA,anyRoles[ROLE_SUPER_USER]
这样配置下来,可以拦截/admin下的所有的url请求,所有请求都会经过kickout定义的过滤器,这里就可以实现,判断session是否过期或当前请求的sessionid是否存在了,然后做其他处理。