简介:踢出用户功能:就是限制一个账号登陆人数。
本文限定一个账号一个用户登陆,并且是挤掉前一个用户
目录
然后配置 ShiroRealm(百度翻译: Realm 领域)
然后 配置踢人(挤人)逻辑 KickoutSessionControlFilter
首先 pom
<!-- shiro -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.2.2</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.2.2</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.2.2</version>
</dependency>
然后Shiro配置Bean ShiroConfigBean
import net.sf.ehcache.CacheException;
import net.sf.ehcache.CacheManager;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.cache.ehcache.EhCacheManager;
import org.apache.shiro.io.ResourceUtils;
import org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO;
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.servlet.SimpleCookie;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;
import java.io.IOException;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* @author liguanhua
* @Date: 2019/12/17 11:21
* @Description: Shiro配置Bean
*/
@Configuration
public class ShiroConfigBean {
private static final String NOT_FILTER_STR = "/login|/photoUpload|/img/|/loginOut|/api/login|/importExcel|/api/deviceGis/add" +
"|/api/log/uploadLog|/upload|/api/app/update|/api/client/update|/api/device/deviceDataByMDCodeL|/land";
private static List<String> NOTFILTER_ARRAY;
static {
NOTFILTER_ARRAY = Arrays.asList(NOT_FILTER_STR.split("\\|"));
}
@Bean
public ShiroFilterFactoryBean shiroFilter(org.apache.shiro.mgt.SecurityManager securityManager) {
System.out.println("ShiroConfiguration.shirFilter()");
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//自定义拦截器 ---------------配置挤人功能呢----------------------
Map<String, Filter> filtersMap = new LinkedHashMap<String, Filter>();
// 限制同一帐号同时在线的个数。
filtersMap.put("kickout", kickoutSessionControlFilter());
shiroFilterFactoryBean.setFilters(filtersMap);
// ----------------------------------------------------
// 必须设置 SecurityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 拦截器.
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
// 设置login URL
shiroFilterFactoryBean.setLoginUrl("/land");
for (String str : NOTFILTER_ARRAY) {
shiroFilterFactoryBean.setLoginUrl(str);
}
// 设置不需要校验的 url
for (String str : NOTFILTER_ARRAY) {
filterChainDefinitionMap.put(str, "anon");
}
filterChainDefinitionMap.put("/Exception.class", "anon");
// 我写的url一般都是xxx.action,根据你的情况自己修改
filterChainDefinitionMap.put("/*", "authc");
// 退出系统的过滤器
filterChainDefinitionMap.put("/loginOut", "logout");
// 最后一班都,固定格式
//其他资源都需要认证 authc 表示需要认证才能进行访问 user表示配置记住我或认证通过可以访问的地址 kickout 挤人功能配置
filterChainDefinitionMap.put("/**", "kickout,authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
/*
* 凭证匹配器 (由于我们的密码校验交给Shiro的SimpleAuthenticationInfo进行处理了
* 所以我们需要修改下doGetAuthenticationInfo中的代码; )
*/
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("md5");// 散列算法:这里使用MD5算法;
hashedCredentialsMatcher.setHashIterations(1);// 散列的次数,比如散列两次,相当于md5(md5(""));
return hashedCredentialsMatcher;
}
// 配置 shiroRealm
@Bean
public ShiroRealm shiroRealm() {
ShiroRealm myShiroRealm = new ShiroRealm();
myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return myShiroRealm;
}
@Bean
public org.apache.shiro.mgt.SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 注入自定义的realm;
securityManager.setRealm(shiroRealm());
// 注入缓存管理器;
securityManager.setCacheManager(ehCacheManager());
//自定义session管理 添加自定义session管理,解决 异常 sessionId(无效)冲突问题。 挤人功能所需配置
securityManager.setSessionManager(sessionManager());
return securityManager;
}
/*
* 开启shiro aop注解支持 使用代理方式;所以需要开启代码支持;
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(
org.apache.shiro.mgt.SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
/**
* DefaultAdvisorAutoProxyCreator,Spring的一个bean,由Advisor决定对哪些类的方法进行AOP代理。
*/
@Bean
@ConditionalOnMissingBean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAAP = new DefaultAdvisorAutoProxyCreator();
defaultAAP.setProxyTargetClass(true);
return defaultAAP;
}
/*
* shiro缓存管理器;
* 需要注入对应的其它的实体类中-->安全管理器:securityManager可见securityManager是整个shiro的核心;
*/
@Bean
public EhCacheManager ehCacheManager() {
EhCacheManager ehcache = new EhCacheManager();
CacheManager cacheManager = CacheManager.getCacheManager("shiro");
if(cacheManager == null){
try {
cacheManager = CacheManager.create(ResourceUtils.getInputStreamForPath("classpath:ehcache.xml"));
} catch (CacheException | IOException e) {
e.printStackTrace();
}
}
ehcache.setCacheManager(cacheManager);
return ehcache;
}
/**
* 限制同一账号登录同时登录人数控制
*
* @return
*/
public KickoutSessionControlFilter kickoutSessionControlFilter() {
KickoutSessionControlFilter kickoutSessionControlFilter = new KickoutSessionControlFilter();
//用于根据会话ID,获取会话进行踢出操作的;
kickoutSessionControlFilter.setSessionManager(sessionManager());
//使用cacheManager获取相应的cache来缓存用户登录的会话;用于保存用户—会话之间的关系的;
kickoutSessionControlFilter.setCacheManager(ehCacheManager());
//是否踢出后来登录的,默认是false;即后者登录的用户踢出前者登录的用户;
kickoutSessionControlFilter.setKickoutAfter(false);
//同一个用户最大的会话数,默认1;比如2的意思是同一个用户允许最多同时两个人登录;
kickoutSessionControlFilter.setMaxSession(1);
//被踢出后重定向到的地址;
kickoutSessionControlFilter.setKickoutUrl("/land?kickout=1");
return kickoutSessionControlFilter;
}
/**
* EnterpriseCacheSessionDAO shiro sessionDao层的实现;
* 提供了缓存功能的会话维护,默认情况下使用MapCache实现,内部使用ConcurrentHashMap保存缓存的会话。
*/
@Bean
public EnterpriseCacheSessionDAO enterCacheSessionDAO() {
EnterpriseCacheSessionDAO enterCacheSessionDAO = new EnterpriseCacheSessionDAO();
//添加缓存管理器
//添加ehcache活跃缓存名称(必须和ehcache缓存名称一致)
enterCacheSessionDAO.setActiveSessionsCacheName("shiro-activeSessionCache");
return enterCacheSessionDAO;
}
@Bean
public SimpleCookie sessionIdCookie() {
//DefaultSecurityManager
SimpleCookie simpleCookie = new SimpleCookie();
//如果在Cookie中设置了"HttpOnly"属性,那么通过程序(JS脚本、Applet等)将无法读取到Cookie信息,这样能有效的防止XSS攻击。
simpleCookie.setHttpOnly(true);
// 用于解决sessionID冲突问题。换个名字
simpleCookie.setName("SHRIOSESSIONID");
//单位秒
simpleCookie.setMaxAge(86400);
return simpleCookie;
}
/**
* @描述:sessionManager添加session缓存操作DAO
* @return
*/
@Bean
public DefaultWebSessionManager sessionManager() {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
sessionManager.setSessionDAO(enterCacheSessionDAO());
sessionManager.setSessionIdCookie(sessionIdCookie());
return sessionManager;
}
}
然后配置 ShiroRealm(百度翻译: Realm 领域)
import com.wulianwang.manage.model.dbentity.system.UserInfo;
import com.wulianwang.manage.service.system.impl.UserService;
import com.wulianwang.manage.utils.util.Utils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.HashSet;
import java.util.Set;
import static com.wulianwang.manage.utils.base.UserUtil.setUser;
/**
* @author liguanhua
* @Date: 2019/12/17 11:23
* @Description:
*/
public class ShiroRealm extends AuthorizingRealm {
/**
* 方面用于加密 参数:AuthenticationToken是从表单穿过来封装好的对象
*/
@Autowired
private UserService userService;
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("doGetAuthenticationInfo:" + token);
// 将AuthenticationToken强转为AuthenticationToken对象
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
// 获得从表单传过来的用户名
String userName = upToken.getUsername();
UserInfo user = userService.selectUserByUserName(userName);
// 如果用户不存在,抛此异常
if (!Utils.isNotEmpty(user)) {
throw new UnknownAccountException("无此用户名!");
}
// 认证的实体信息,可以是username,也可以是用户的实体类对象,这里用的用户名
Object principal = userName;
// 从数据库中查询的密码
Object credentials = user.getPassword();
// 当前realm对象的名称,调用分类的getName()
String realmName = this.getName();
// 创建SimpleAuthenticationInfo对象,并且把username和password等信息封装到里面
// 用户密码的比对是Shiro帮我们完成的
SimpleAuthenticationInfo info = null;
info = new SimpleAuthenticationInfo(principal, credentials, realmName);
setUser(user);
return info;
}
// 用于授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
System.out.println("MyShiroRealm的doGetAuthorizationInfo授权方法执行");
// User user=(User)
// principals.fromRealm(this.getClass().getName()).iterator().next();//获取session中的用户
// System.out.println("在MyShiroRealm中AuthorizationInfo(授权)方法中从session中获取的user对象:"+user);
// 从PrincipalCollection中获得用户信息
Object principal = principals.getPrimaryPrincipal();
System.out.println("ShiroRealm AuthorizationInfo:" + principal.toString());
// 根据用户名来查询数据库赋予用户角色,权限(查数据库)
Set<String> roles = new HashSet<>();
Set<String> permissions = new HashSet<>();
// 2018.09.14更新
// 给用户添加user权限 (没有进行判断、对所有的用户给user权限)
// if("user".equals(principal)){
// roles.add("user");
// permissions.add("user:query");
// }
//// 当用户名为admin时 为用户添加权限admin 两个admin可以理解为连个字段
// if ("admin".equals(principal)) {
// roles.add("admin");
// permissions.add("admin:query");
// }
//// 为用户添加visit游客权限,在url中没有为visit权限,所以,所有的操作都没权限
// if("visit".equals(principal)){
// roles.add("visit");
// permissions.add("visit:query");
// }
// 更新以上代码
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roles);
//添加权限
info.setStringPermissions(permissions);
return info;
// return null;
}
}
然后sessiondao SessionDAO
import org.apache.shiro.session.Session;
import org.apache.shiro.session.UnknownSessionException;
import java.io.Serializable;
import java.util.Collection;
/**
* @author liguanhua
* @Date: 2019/12/19 10:36
* @Description:
*/
public interface SessionDAO {
/*如DefaultSessionManager在创建完session后会调用该方法;
如保存到关系数据库/文件系统/NoSQL数据库;即可以实现会话的持久化;
返回会话ID;主要此处返回的ID.equals(session.getId());
*/
Serializable create(Session session);
//根据会话ID获取会话
Session readSession(Serializable sessionId) throws UnknownSessionException;
//更新会话;如更新会话最后访问时间/停止会话/设置超时时间/设置移除属性等会调用
void update(Session session) throws UnknownSessionException;
//删除会话;当会话过期/会话停止(如用户退出时)会调用
void delete(Session session);
//获取当前所有活跃用户,如果用户量多此方法影响性能
Collection<Session> getActiveSessions();
}
然后 ehcache.xml配置 boot 2.0以上应该是需要加 defaultCache 配置。如果你报错提示 default 错误可以加上这个

<ehcache>
<diskStore path="java.io.tmpdir"/>
<!-- name 缓存名称 -->
<!-- maxElementsInMemory 内存中最大缓存对象数,看着自己的heap大小来搞 -->
<!-- eternal:true表示对象永不过期,此时会忽略timeToIdleSeconds和timeToLiveSeconds属性,默认为false -->
<!-- maxElementsOnDisk:硬盘中最大缓存对象数,若是0表示无穷大 -->
<!-- overflowToDisk:true表示当内存缓存的对象数目达到了maxElementsInMemory界限后,
会把溢出的对象写到硬盘缓存中。注意:如果缓存的对象要写入到硬盘中的话,则该对象必须实现了Serializable接口才行。-->
<!-- diskSpoolBufferSizeMB:磁盘缓存区大小,默认为30MB。每个Cache都应该有自己的一个缓存区。-->
<!-- diskPersistent:是否缓存虚拟机重启期数据 -->
<!-- diskExpiryThreadIntervalSeconds:磁盘失效线程运行时间间隔,默认为120秒 -->
<!--timeToIdleSeconds:设置对象在失效前的允许闲置时间(单位:秒)。当对象自从最近一次被访问后,
如果处于空闲状态的时间超过了timeToIdleSeconds属性值,这个对象就会过期,
EHCache将把它从缓存中清空。仅当eternal=false对象不是永久有效时使用,可选属性,默认值是0,也就是可闲置时间无穷大。-->
<!--timeToLiveSeconds:设置对象在失效前允许存活时间(单位:秒)。最大时间介于创建时间和失效时间之间。
如果处于缓存中的时间超过了 timeToLiveSeconds属性值,这个对象就会过期,
EHCache将把它从缓存中清除。仅当eternal=false对象不是永久有效时使用,默认是0.,也就是对象存活时间无穷大。-->
<!-- clearOnFlush:内存数量最大时是否清除 -->
<!-- memoryStoreEvictionPolicy:当达到maxElementsInMemory限制时,
Ehcache将会根据指定的策略去清理内存。可选策略有:LRU(最近最少使用,默认策略)、
FIFO(先进先出)、LFU(最少访问次数)。--><!-- shiro-activeSessionCache活跃用户session缓存策略 -->
<defaultCache
maxElementsInMemory="10000"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
maxElementsOnDisk="10000000"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU">
<!--<persistence strategy="localTempSwap"/>-->
</defaultCache>
<cache name="shiro-activeSessionCache"
maxElementsInMemory="10000"
timeToIdleSeconds="86400"
timeToLiveSeconds="86400"
maxElementsOnDisk="10000000"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU">
<!-- <persistence strategy="localTempSwap"/>-->
</cache>
<!-- 登录记录缓存 锁定2分钟 -->
<cache name="passwordRetryCache"
maxEntriesLocalHeap="10000"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="0"
overflowToDisk="false"
statistics="false">
</cache>
</ehcache>
然后 配置踢人(挤人)逻辑 KickoutSessionControlFilter
import java.io.Serializable;
import java.util.ArrayDeque;
import java.util.Deque;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @author liguanhua
* @Date: 2019/12/18 14:32
* @Description: 挤人逻辑
*/
public class KickoutSessionControlFilter extends AccessControlFilter {
private static final Logger logger = LoggerFactory
.getLogger(KickoutSessionControlFilter.class);
private static SessionDAO sessionDAO;
private String kickoutUrl; // 踢出后到的地址
private boolean kickoutAfter = false; // 踢出之前登录的/之后登录的用户 默认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) {
//必须和ehcache缓存配置中的缓存name一致
this.cache = cacheManager.getCache("shiro-activeSessionCache");
}
@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();
logger.debug("==session时间设置:" + String.valueOf(session.getTimeout())
+ "===========");
try {
// 当前用户
String username = subject.getPrincipal() + "";
logger.debug("===当前用户username:==" + username);
Serializable sessionId = session.getId();
logger.debug("===当前用户sessionId:==" + sessionId);
// 读取缓存用户 没有就存入
Deque<Serializable> deque = cache.get(username);
logger.debug("===当前deque:==" + deque);
if (deque == null) {
// 初始化队列
deque = new ArrayDeque<Serializable>();
}
// 如果队列里没有此sessionId,且用户没有被踢出;放入队列
if (!deque.contains(sessionId)
&& session.getAttribute("kickout") == null) {
// 将sessionId存入队列
deque.push(sessionId);
// 将用户的sessionId队列缓存
cache.put(username, deque);
}
// 如果队列里的sessionId数超出最大会话数,开始踢人
while (deque.size() > maxSession) {
logger.debug("===deque队列长度:==" + deque.size());
Serializable kickoutSessionId = null;
// 是否踢出后来登录的,默认是false;即后者登录的用户踢出前者登录的用户;
if (kickoutAfter) { // 如果踢出后者
kickoutSessionId = deque.removeFirst();
} else { // 否则踢出前者
kickoutSessionId = deque.removeLast();
}
// 踢出后再更新下缓存队列
cache.put(username, deque);
try {
// 获取被踢出的sessionId的session对象
Session kickoutSession = sessionManager
.getSession(new DefaultSessionKey(kickoutSessionId));
if (kickoutSession != null) {
// 设置会话的kickout属性表示踢出了
kickoutSession.setAttribute("kickout", true);
}
} catch (Exception e) {// ignore exception
e.printStackTrace();
}
}
// 如果被踢出了,(前者或后者)直接退出,重定向到踢出后的地址
if ((Boolean) session.getAttribute("kickout") != null
&& (Boolean) session.getAttribute("kickout") == true) {
// 会话被踢出了
try {
// 退出登录
subject.logout();
} catch (Exception e) { // ignore
}
saveRequest(request);
logger.debug("==踢出后用户重定向的路径kickoutUrl:" + kickoutUrl);
// 重定向
WebUtils.issueRedirect(request, response, kickoutUrl);
return false;
}
return true;
} catch (Exception e) { // ignore
//重定向到登录界面
WebUtils.issueRedirect(request, response, "/land");
return false;
}
}
}
UnknownSessionException异常原因
只所以出现这个问题是因为在shiro的DefaultWebSessionManager类中,默认Cookie名称是JSESSIONID,这样的话与servlet容器名冲突, 如jetty, tomcat等默认JSESSIONID, 当跳出shiro servlet时如error-page容器会为JSESSIONID重新分配值导致登录会话丢失!
我们只需要自己指定一个与项目运行容器不冲突的sessionID就好了
上面配置sessionManager(sessionIdCookie)就是为了解决这个问题。使用了SHRIOSESSIONID
service
UserService
/**
* 根据用户名获取用户信息
* @param userName
* @return
*/
public UserInfo selectUserByUserName(String userName){
UserInfo user = userMapper.selectUserByUserName(userName);
return user;
}
@RestController
@PostMapping(value = "/login")
public Message userLogin(@RequestParam Map<String, String> param, HttpSession session) {
// 获得当前Subject
Subject currentUser = SecurityUtils.getSubject();
String userName = param.get("userName");
String password = param.get("password");
// 验证用户是否验证,即是否登录
// if (!currentUser.isAuthenticated()) {
String msg = "";
// 把用户名和密码封装为 UsernamePasswordToken 对象
UsernamePasswordToken token = new UsernamePasswordToken(userName, password);
// remembermMe记住密码 关闭浏览器后再次打开浏览器。系统将Cookie 存到本地。可以不用登陆直接进入
token.setRememberMe(true);
try {
// 执行登录.
currentUser.login(token);
return renderSuccess(map);
} catch (IncorrectCredentialsException e) {
return renderError(Global.NAME_OR_PWD_ERROR);
} catch (ExcessiveAttemptsException e) {
msg = "登录失败次数过多";
return renderError(msg);
} catch (LockedAccountException e) {
msg = "帐号已被锁定";
return renderError(msg);
} catch (DisabledAccountException e) {
return renderError(Global.DISABLED_MESSAGE);
} catch (ExpiredCredentialsException e) {
msg = "帐号已过期";
return renderError(msg);
} catch (UnknownAccountException e) {
msg = "帐号不存在";
return renderError(msg);
} catch (UnauthorizedException e) {
msg = "您没有得到相应的授权";
return renderError(msg);
} catch (Exception e) {
return renderError(msg);
}
// }
// return renderSuccess("您已登陆");
}
记住我

其他详细权限等介绍可看 https://blog.youkuaiyun.com/qq_32786139/article/details/82658197

本文介绍了如何使用Spring Boot和Shiro实现限制同一账号只能一个用户登录并挤出已登录用户的功能,同时解决了UnknownSessionException异常问题。详细步骤包括配置pom、ShiroConfigBean、ShiroRealm、SessionDAO、KickoutSessionControlFilter,以及服务层和服务控制器的设置。此外,还提供了Remember Me功能的提示。
1475





