今天又是美好的一天,好了,不多逼逼。今天内容有点多,咱们抓紧时间。
好,咱们来讲讲如何写springboot+shiro 实现登陆人数控制
来,咱们直接看代码
conrtoller:咱们就是一个正常登陆,没什么区别
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(username,password);
System.out.println("登录成功");
subject.login(usernamePasswordToken);
MyRealm看一看就可以了,没什么改动
KickoutSessionControlFilter 它是核心,我copy,不要喷我
public class KickoutSessionControlFilter extends AccessControlFilter {
/** 踢出后到的地址 */
private String kickoutUrl;
/** 踢出之前登录的/之后登录的用户 默认踢出之前登录的用户 */
private boolean kickoutAfter = false;
/** 同一个帐号最大会话数 默认1 */
private int maxSession = 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;
}
public void setCacheManager(CacheManager cacheManager) {
this.cache = cacheManager.getCache("shiro-activeSessionCache");
}
/**
* 是否允许访问,返回true表示允许
*/
@Override
protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o)
throws Exception {
return false;
}
/**
* 表示访问拒绝时是否自己处理,如果返回true表示自己不处理且继续拦截器链执行,返回false表示自己已经处理了(比如重定向到另一个页面)。
*/
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response)
throws Exception {
System.out.println("=======================我进来了================================");
Subject subject = getSubject(request, response);
System.out.println(subject);
System.out.println(subject.isAuthenticated() && !subject.isRemembered());
if(!subject.isAuthenticated() && !subject.isRemembered()) {
//如果没有登录,直接进行之后的流程
return true;
}
Session session = subject.getSession();
//这里获取的User是实体 因为我在 自定义ShiroRealm中的doGetAuthenticationInfo方法中
//new SimpleAuthenticationInfo(user, password, getName()); 传的是 User实体 所以这里拿到的也是实体,如果传的是userName 这里拿到的就是userName
String username = ((String) subject.getPrincipal());
Serializable sessionId = session.getId();
System.out.println(sessionId);
// 初始化用户的队列放到缓存里
Deque<Serializable> deque = cache.get(username);
if(deque == null) {
deque = new LinkedList<Serializable>();
cache.put(username, deque);
}
//如果队列里没有此sessionId,且用户没有被踢出;放入队列
if(!deque.contains(sessionId) && session.getAttribute("kickout") == null) {
deque.push(sessionId);
}
//如果队列里的sessionId数超出最大会话数,开始踢人
System.out.println(deque.size());
while(deque.size() > maxSession) {
Serializable kickoutSessionId = null;
if(kickoutAfter) { //如果踢出后者
kickoutSessionId=deque.getFirst();
kickoutSessionId = deque.removeFirst();
System.out.println("===============================踢出去了=============================");
} else { //否则踢出前者
kickoutSessionId = deque.removeLast();
System.out.println("===============================踢出去了=============================");
}
try {
Session kickoutSession = sessionManager.getSession(new DefaultSessionKey(kickoutSessionId));
if(kickoutSession != null) {
//设置会话的kickout属性表示踢出了
kickoutSession.setAttribute("kickout", true);
}
} catch (Exception e) {//ignore exception
e.printStackTrace();
}
}
//如果被踢出了,直接退出,重定向到踢出后的地址
if (session.getAttribute("kickout") != null) {
//会话被踢出了
try {
subject.logout();
} catch (Exception e) {
}
WebUtils.issueRedirect(request, response, kickoutUrl);
return false;
}
return true;
}
}
简单说一下继承AccessControlFilter接口,重写里面的两个方法,isAccessAllowed return 返回true 的话就直接跑了,不会拦截,所以咱们都会设置成false,这样子,他就会跑到onAccessDenied 里面,就会执行咱们的代码了。
**
注意:
**
this.cache = cacheManager.getCache(“shiro-activeSessionCache”); 这个地方注意一下,如果你这个cacheManager.getCache(“shiro-activeSessionCache”);地方话有一个地方需要注意,private Cache<String, Deque> cache; 导包要shiro的那个,别导错了,否则会飘红…
ShiroConfig
//自定义MyRealm
@Bean
public MyRealm myAuthRealm(){
try {
MyRealm myRealm = new MyRealm();
System.out.println("======================请稍后====================================================");
Thread.sleep(2000);
System.out.println("=================================myRealm注册完成================================");
Thread.sleep(1000);
return myRealm;
} catch (InterruptedException e) {
e.printStackTrace();
}
return null;
}
/**
* 控制并发人数
* @return
*/
@Bean
public KickoutSessionControlFilter kickoutSessionControlFilter(){
System.out.println("============控制并发人数");
KickoutSessionControlFilter kickoutSessionControlFilter = new KickoutSessionControlFilter();
//用于根据会话ID,获取会话进行踢出操作的;
kickoutSessionControlFilter.setSessionManager(securityManager());
//使用cacheManager获取相应的cache来缓存用户登录的会话;用于保存用户—会话之间的关系的;
kickoutSessionControlFilter.setCacheManager(ehCacheManager());
//是否踢出后来登录的,默认是false;即后者登录的用户踢出前者登录的用户;
kickoutSessionControlFilter.setKickoutAfter(false);
//同一个用户最大的会话数,默认1;比如2的意思是同一个用户允许最多同时两个人登录;
kickoutSessionControlFilter.setMaxSession(1);
//被踢出后重定向到的地址;
kickoutSessionControlFilter.setKickoutUrl("/user/pcLogin?kickout=1");
return kickoutSessionControlFilter;
}
/**
* 缓存
* @return
*/
@Bean
public EhCacheManager ehCacheManager(){
EhCacheManager cacheManager = new EhCacheManager();
cacheManager.setCacheManagerConfigFile("classpath:ehcache.xml");
return cacheManager;
}
额。。。 这个xml文件我怎么给你??(网上找找,找不到去我网盘里面拿去吧!)
需要放在resources下面
点击这里宝贝
提取码:nscb
@Bean
public EnterpriseCacheSessionDAO enterCacheSessionDAO() {
EnterpriseCacheSessionDAO enterCacheSessionDAO = new EnterpriseCacheSessionDAO();
//添加缓存管理器
//enterCacheSessionDAO.setCacheManager(ehCacheManager());
//添加ehcache活跃缓存名称(必须和ehcache缓存名称一致)
enterCacheSessionDAO.setActiveSessionsCacheName("shiro-activeSessionCache");
return enterCacheSessionDAO;
}
//配置安全管理器 SecurityManager:
@Bean
public DefaultWebSecurityManager securityManager() {
try {
// 将自定义 Realm 加进来
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(myAuthRealm());
System.out.println("======================请稍后====================================================");
Thread.sleep(2000);
System.out.println("==============================securityManager注册完成===========================================");
Thread.sleep(1000);
return securityManager;
} catch (InterruptedException e) {
e.printStackTrace();
}
return null;
}
如果你产生了这个错误:org.apache.shiro.UnavailableSecurityManagerException: No SecurityManager accessible to the calling code, either bound to the org.apache.shiro.util.ThreadContext or as a vm static singleton. This is an invalid application configuration.,请添加下面这段代码。
具体原因,不详。如有大佬知道为啥,请在底下告诉我。 感激不尽!!!
/**
* 添加万这个就会让控制登录接口不回报错,原因不详
* @return
*/
@Bean
public FilterRegistrationBean delegatingFilterProxy(){
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
DelegatingFilterProxy proxy = new DelegatingFilterProxy();
proxy.setTargetFilterLifecycle(true);
proxy.setTargetBeanName("shiroFilter");
filterRegistrationBean.setFilter(proxy);
return filterRegistrationBean;
}
filterChainMap.put("/**", “kickout,authc”); 这个全部拦截啥的 前面需要加上kickout。
@Bean
public ShiroFilterFactoryBean shiroFilter(DefaultWebSecurityManager securityManager){
// 定义 shiroFactoryBean
ShiroFilterFactoryBean shiroFilterFactoryBean=new ShiroFilterFactoryBean();
// 设置自定义的 securityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 设置默认登录的 URL,身份认证失败会访问该 URL
shiroFilterFactoryBean.setLoginUrl("/user/pcLogin");
//自定义拦截器限制并发人数
LinkedHashMap<String, Filter> filtersMap = new LinkedHashMap<>();
//限制同一帐号同时在线的个数
filtersMap.put("kickout", kickoutSessionControlFilter());
shiroFilterFactoryBean.setFilters(filtersMap);
// LinkedHashMap 是有序的,进行顺序拦截器配置
Map<String,String> filterChainMap = new LinkedHashMap<>();
// 配置可以匿名访问的地址,可以根据实际情况自己添加,放行一些静态资源等,anon 表示放行
filterChainMap.put("/swagger-*/**", "anon");
filterChainMap.put("/v2/**", "anon");
filterChainMap.put("/swagger-resources/**", "anon");
filterChainMap.put("/swagger-ui.html/**", "anon");
// 登录 URL 放行
filterChainMap.put("/user/pclogin", "anon");
//全部拦截(认证)
filterChainMap.put("/**", "kickout,authc");
// 设置 shiroFilterFactoryBean 的 FilterChainDefinitionMap
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainMap);
System.out.println("=======================================shiroFilterFactoryBean注册完成===================================");
return shiroFilterFactoryBean;
}