简介
- 在某些项目中,一个账户可能只允许一人登录或几个人同时登录,当超过人数时,要么进制后者登录,要么踢出前者登录。
- Spring Security直接提供了相应的功能。
- Shiro没有提供默认实现,但自己添加也是很容易的。
示例
- 采用第十六章的示例,主要功能是用户、公司、角色和资源之间关系的调整,如修改用户所属公司、所有角色,资源所需权限、角色所有资源等。
- 目的:添加登录人数限制功能。
- KickoutSessionControlFilter:控制登录人数的过滤器。
public class KickoutSessionControllerFilter extends AccessControlFilter {
//踢出后重定向的地址
private String kickoutUrl;
//是否踢出之后登录的用户,默认false,即踢出之前登录的而用户
private boolean kickoutAfter=false;
//同一账号最大会话数,默认1
private int maxSession=1;
//根据会话id,获取会话进行踢出操作
private SessionManager sessionManager;
//使用cacheManager获取相应cache来缓存用户登录的会话,用于保存用户-会话之间的关系
private Cache<String,Deque<Serializable>> cache;
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response,
Object mappedValue) throws Exception {
return false;
}
public void setCacheManager(CacheManager cacheManager) {
//根据name获取cache实例,没有则新建一个
this.cache = cacheManager.getCache("shiro-kickout-session");
}
//其他setter
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response)
throws Exception {
Subject subject=getSubject(request,response);
//如果未登录,则直接进行之后的流程
if(!subject.isAuthenticated()&&!subject.isRemembered())
return true; //打断点,看到底和false什么区别!!!
Session session=subject.getSession();
Serializable sessionId=session.getId();
String username=(String)subject.getPrincipal();
//获取之前登录者队列
Deque<Serializable> deque=cache.get(username); //若有多人登录,
//则查询key为username的一条目,其value为包含多个会话的队列
if(deque==null){
deque=new LinkedList<>();
//第一次暂时存储了一个空队列进去,但下一个if将放入会话,
//所以第一次存储了一个包含自身会话的队列
cache.put(username,deque); //存储时键为username,值为之前登录者会话队列
}
//如果队列里没有此sessionId,且用户没有被踢出,则放入队列
if(!deque.contains(sessionId) && session.getAttribute("kickout")==null)
deque.push(sessionId); //插入队头,而队列的引用放在cache中,相当于缓存会话
//如果队列里的会话数超出最大值,则开始踢人
while (deque.size()>maxSession){ //用if应该就可以,顶多加入当前用户即超出限制
//要被踢的sessionId
Serializable kickoutSessionId=null;
//如果规定踢出后者
if(kickoutAfter)
kickoutSessionId=deque.removeFirst(); //弹出首元素,此处为最后进去的元素
//否则踢出前者
else
kickoutSessionId=deque.removeLast(); //弹出尾元素
try {
//根据session id获取session
Session kickoutSession = sessionManager
.getSession(new DefaultSessionKey(kickoutSessionId));
if (kickoutSession != null)
//设置会话的kickout属性表示被踢出
kickoutSession.setAttribute("kickout", true);
} catch (Exception e){
//ignore
}
}
//如果当前会话不幸在上面while中被踢出了,则注销并重定向到踢出后的地址;
//但如果踢出的是其他会话,那么需要刷新,它才会被强制登出
if(session.getAttribute("kickout")!=null){
try {
subject.logout();
} catch (Exception e){
//ignore
}
saveRequest(request); //保存当前请求
WebUtils.issueRedirect(request,response,kickoutUrl); //返回login页面
return false;
}
return true;
}
}
ehcache.xml:设置用于存放会话的缓存shiro-kickout-session。
<cache name="shiro-kickout-session" maxEntriesLocalHeap="2000" eternal="false" timeToIdleSeconds="3600" timeToLiveSeconds="0" overflowToDisk="false" statistics="true"> </cache>
spring-config-shiro.xml: 注册过滤器。
<!--控制并发登录人数的过滤器--> <bean id="kickoutSessionControllerFilter" class="com.haien.chapter18.web.shiro.filter.KickoutSessionControllerFilter"> <!--使用cacheManager获取相应cache来缓存用户登录的会话, 用于保存用户-会话之间的关系--> <property name="cacheManager" ref="cacheManager"/> <!--用于根据会话id,获取会话进行踢出操作--> <property name="sessionManager" ref="sessionManager"/> <!--是否踢出后来登录的,默认false--> <property name="kickoutAfter" value="false"/> <!--同一个用户最大会话数,默认1,即只能一人登录--> <property name="maxSession" value="2"/> <!--被踢出后重定向到的地址--> <property name="kickoutUrl" value="/login?kickout=1"/> </bean> <!-- Shiro的Web过滤器 --> <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <property name="securityManager" ref="securityManager"/> <property name="loginUrl" value="/login"/> <property name="filters"> <util:map> <entry key="authc" value-ref="formAuthenticationFilter"/> <entry key="sysUser" value-ref="sysUserFilter"/> <entry key="kickout" value-ref="kickoutSessionControllerFilter"/> </util:map> </property> <property name="filterChainDefinitions"> <value> /login = authc /logout = logout /authenticated = authc /** = kickout,user,sysUser </value> </property> </bean>
测试:本例设置maxSession=2,所以需要打开3个浏览器,分别访问localhost:8080/chapter18进行登录,然后刷新第一次登录的浏览器,将会被强制退出返回登录页面。
- 代码示例:ideaProjects/shiro-chapter18