全栈编程系列SpringBoot整合Shiro(含KickoutSessionControlFilter并发在线人数控制以及不生效问题配置启动异常No SecurityManager...)(代码片段
Posted 善良勤劳勇敢而又聪明的老杨
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了全栈编程系列SpringBoot整合Shiro(含KickoutSessionControlFilter并发在线人数控制以及不生效问题配置启动异常No SecurityManager...)(代码片段相关的知识,希望对你有一定的参考价值。
热门系列:
目录
2.3 shiro并发在线人数控制KickoutSessionControlFilter
2.3.1 自定义shiro的KickoutSessionControlFilter请求时报错异常
2.3.2 KickoutSessionControlFilter并发在线人数控制失效
1、前言
好久没整活儿了。最近在整合shiro的时候,出现了诸多问题。还有一些实践总结,趁闲余时间记录一下。也希望能帮到遇到相同问题的老哥们,下面开整~~~
2、正文
2.1 环境介绍
这里使用的是前后端分离。前端页面采用html结合thymeleaf。依赖包版本配置如下:
<!--springboot版本-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<!--shiro依赖版本-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.2</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.4.2</version>
</dependency>
<!--thymeleaf整合shiro-->
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.0.0</version>
</dependency>
2.2 shiro配置
使用相对还算简单,直接贴下代码!权限控制类AccountRealm:
package com.yangy.web.realm;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.yangy.web.constant.CommonConstants;
import com.yangy.web.entity.User;
import com.yangy.web.mapper.UserMapper;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.UsernamePasswordToken;
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.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
/**
* @author yangy
* @description 这是shiro的认证和授权规则,必须继承 AuthorizingRealm类
*/
public class AccountRealm extends AuthorizingRealm {
@Autowired
private UserMapper userMapper;
//先认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(
AuthenticationToken authenticationToken) throws AuthenticationException {
//客户端传过来的对象和密码,自动封装在token中,
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
//根据用户名进行查询,并且判断
User user = userMapper.selectOne(new QueryWrapper<User>().eq("login_name", String.valueOf(token.getUsername())));
if(Objects.nonNull(user)){
//用户名不为空,继续验证密码
return new SimpleAuthenticationInfo(user,user.getPassword(),getName());
}
//用户名为空,直接跳出验证
return null;
}
//再授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(
PrincipalCollection principalCollection) {
//获取当前用户邓登陆的信息
Subject subject = SecurityUtils.getSubject();
User user = (User) subject.getPrincipal();
//设置角色
Set<String> roles = new HashSet<>();
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
if(Objects.nonNull(user)){
if(user.getType().intValue() == CommonConstants.USER_TYPE_OF_TOURIST){
roles.add("tourist");
info.setRoles(roles);
//设置权限
info.addStringPermission("/delete");
}else if(user.getType().intValue() == CommonConstants.USER_TYPE_OF_MANAGE){
roles.add("manage");
info.setRoles(roles);
}
}
return info;
}
}
shiro主配置类
package com.yangy.web.config;
import at.pollux.thymeleaf.shiro.dialect.ShiroDialect;
import com.yangy.web.realm.AccountRealm;
import com.yangy.web.realm.KickoutSessionControlFilter;
import com.yangy.web.realm.RedisCacheManager;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import javax.servlet.Filter;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* @author Yangy
* 这是shiro的配置文件
* (1)anon:匿名过滤器,表示通过了url配置的资源都可以访问,例:“/statics/**=anon”表示statics目录下所有资源都能访问
* (2)authc:基于表单的过滤器,表示通过了url配置的资源需要登录验证,否则跳转到登录,例:“/unauthor.jsp=authc”如果用户没有登录访问unauthor.jsp则直接跳转到登录
* (3)authcBasic:Basic的身份验证过滤器,表示通过了url配置的资源会提示身份验证,例:“/welcom.jsp=authcBasic”访问welcom.jsp时会弹出身份验证框
* (4)perms:权限过滤器,表示访问通过了url配置的资源会检查相应权限,例:“/statics/**=perms["user:add:*,user:modify:*"]“表示访问statics目录下的资源时只有新增和修改的权限
* (5)port:端口过滤器,表示会验证通过了url配置的资源的请求的端口号,例:“/port.jsp=port[8088]”访问port.jsp时端口号不是8088会提示错误
* (6)rest:restful类型过滤器,表示会对通过了url配置的资源进行restful风格检查,例:“/welcom=rest[user:create]”表示通过restful访问welcom资源时只有新增权限
* (7)roles:角色过滤器,表示访问通过了url配置的资源会检查是否拥有该角色,例:“/welcom.jsp=roles[admin]”表示访问welcom.jsp页面时会检查是否拥有admin角色
* (8)ssl:ssl过滤器,表示通过了url配置的资源只能通过https协议访问,例:“/welcom.jsp=ssl”表示访问welcom.jsp页面如果请求协议不是https会提示错误
* (9)user:用户过滤器,表示可以使用登录验证/记住我的方式访问通过了url配置的资源,例:“/welcom.jsp=user”表示访问welcom.jsp页面可以通过登录验证或使用记住我后访问,否则直接跳转到登录
* (10)logout:退出拦截器,表示执行logout方法后,跳转到通过了url配置的资源,例:“/logout.jsp=logout”表示执行了logout方法后直接跳转到logout.jsp页面
*/
@Configuration
public class ShiroConfig {
//注入realm
@Bean
public AccountRealm accountRealm() {
return new AccountRealm();
}
//注入redisCacheManager,供shiro管控使用
@Bean
public RedisCacheManager redisCacheManager(
@Qualifier("redisTemplate") RedisTemplate redisTemplate) {
RedisCacheManager redisCacheManager = new RedisCacheManager();
redisCacheManager.setCacheLive(120);
redisCacheManager.setRedisTemplate(redisTemplate);
return redisCacheManager;
}
/**
* 自定义sessionManager
*
* @return
*/
@Bean
public DefaultWebSessionManager sessionManager() {
DefaultWebSessionManager shiroSessionManager = new DefaultWebSessionManager();
//删除失效session
shiroSessionManager.setDeleteInvalidSessions(true);
return shiroSessionManager;
}
//注入安全管理器
@Bean
public DefaultWebSecurityManager securityManager(
@Qualifier("accountRealm") AccountRealm accountRealm,
@Qualifier("sessionManager") DefaultWebSessionManager sessionManager) {
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
manager.setRealm(accountRealm);
manager.setSessionManager(sessionManager);
return manager;
}
//注入工厂
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(
@Qualifier("securityManager") DefaultWebSecurityManager securityManager,
@Qualifier("kickoutSessionControlFilter") KickoutSessionControlFilter kickoutSessionControlFilter) {
ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
factoryBean.setSecurityManager(securityManager);
//自定义拦截器限制并发人数,参考博客
LinkedHashMap<String, Filter> filtersMap = new LinkedHashMap<>();
//限制同一帐号同时在线的个数
filtersMap.put("kickout", kickoutSessionControlFilter);
factoryBean.setFilters(filtersMap);
//权限设置
Map<String, String> map = new LinkedHashMap<>();
map.put("/admin", "anon");
map.put("/admin/login.do", "anon");
map.put("/payRecord/pay.do", "anon");
map.put("/admin/index.do", "kickout,authc");
map.put("/view/list.do", "kickout,authc");
map.put("/line/list.do", "kickout,authc");
map.put("/order/manageList.do", "kickout,authc");
map.put("/user/private/**", "kickout,authc");
map.put("/leaveMessage/**", "kickout,authc");
map.put("/payRecord/toPay.do", "kickout,authc");
map.put("/order/**", "kickout,authc");
map.put("/**", "kickout");
map.put("/manage", "perms[manage]");
map.put("/administrator", "roles[administrator]");
factoryBean.setFilterChainDefinitionMap(map);
//设置登陆页面
factoryBean.setLoginUrl("/f_login.html");
//设置未授权页面
factoryBean.setUnauthorizedUrl("/unauth");
return factoryBean;
}
/**
* 并发登录控制
*
* @return
*/
@Bean
public KickoutSessionControlFilter kickoutSessionControlFilter(
@Qualifier("sessionManager") DefaultWebSessionManager sessionManager,
@Qualifier("redisCacheManager") RedisCacheManager redisCacheManager) {
KickoutSessionControlFilter kickoutSessionControlFilter = new KickoutSessionControlFilter();
//用于根据会话ID,获取会话进行踢出操作的;
kickoutSessionControlFilter.setSessionManager(sessionManager);
//使用cacheManager获取相应的cache来缓存用户登录的会话;用于保存用户—会话之间的关系的;
kickoutSessionControlFilter.setCacheManager(redisCacheManager);
//是否踢出后来登录的,默认是false;即后者登录的用户踢出前者登录的用户;
kickoutSessionControlFilter.setKickoutAfter(false);
//同一个用户最大的会话数,默认1;比如2的意思是同一个用户允许最多同时两个人登录;
kickoutSessionControlFilter.setMaxSession(1);
//被踢出后重定向到的地址;
kickoutSessionControlFilter.setKickoutUrl("/user/login.do?kickout=1");
return kickoutSessionControlFilter;
}
//shiro整合thymeleaf,要想在html中使用shiro,就必须先引进方言。
@Bean
public ShiroDialect shiroDialect() {
return new ShiroDialect();
}
}
做完上面的配置,基本就可以完成简单的权限控制搭建了。。根据各自的需要,可进行相应的配置即可。
2.3 shiro并发在线人数控制KickoutSessionControlFilter
其实今天,主要想记录的是这一块。因为这一块整合的时候,遇到不少问题,耗费了不少时间。下面逐一讲一下,主要有两个问题:
- 整合自定义KickoutSessionControlFilter时请求发生异常
- KickoutSessionControlFilter并发在线人数控制失效,没作用
2.3.1 自定义shiro的KickoutSessionControlFilter请求时报错异常
先贴一下自定义的KickoutSessionControlFilter类代码,如下:
package com.yangy.web.realm;
import com.yangy.web.entity.User;
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 javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.Serializable;
import java.util.Deque;
import java.util.LinkedList;
/**
* @Author: Yangy
* @Date: 2021/8/30 16:11
* @Description
*/
public class KickoutSessionControlFilter extends AccessControlFilter {
private String kickoutUrl; //踢出后到的地址
private boolean kickoutAfter = 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;
}
public void setCacheManager(CacheManager cacheManager) {
this.cache = cacheManager.getCache("shiro-kickout-session");
}
@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();
User user = (User) subject.getPrincipal();
String username = user.getUserName();
Serializable sessionId = session.getId();
// 同步控制
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数超出最大会话数,开始踢人
while(deque.size() > maxSession) {
Serializable kickoutSessionId = null;
if(kickoutAfter) { //如果踢出后者
kickoutSessionId = deque.removeFirst();
} else { //否则踢出前者
kickoutSessionId = deque.removeLast();
}
try {
Session kickoutSession = sessionManager.getSession(new DefaultSessionKey(kickoutSessionId));
if(kickoutSession != null) {
//设置会话的kickout属性表示踢出了
kickoutSession.setAttribute("kickout", true);
}
} catch (Exception e) {//ignore exception
}
}
//如果被踢出了,直接退出,重定向到踢出后的地址
if (session.getAttribute("kickout") != null) {
//会话被踢出了
try {
subject.logout();
} catch (Exception e) {
}
WebUtils.issueRedirect(request, response, kickoutUrl);
return false;
}
return true;
}
}
此时,基本是配置完成了。也已设置了同一账户最大在线人数为1,请求一下试试。结果后台直接500了,报了如下异常:
ERROR 26544 --- [nio-9001-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [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.] with root cause
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.
at org.apache.shiro.SecurityUtils.getSecurityManager(SecurityUtils.java:123) ~[shiro-core-1.4.2.jar:1.4.2]
at org.apache.shiro.subject.Subject$Builder.<init>(Subject.java:626) ~[shiro-core-1.4.2.jar:1.4.2]
at org.apache.shiro.SecurityUtils.getSubject(SecurityUtils.java:56) ~[shiro-core-1.4.2.jar:1.4.2]
at org.apache.shiro.web.filter.AccessControlFilter.getSubject(AccessControlFilter.java:97) ~[shiro-web-1.4.2.jar:1.4.2]
at com.yangy.web.realm.KickoutSessionControlFilter.onAccessDenied(KickoutSessionControlFilter.java:60) ~[classes/:na]
at org.apache.shiro.web.filter.AccessControlFilter.onAccessDenied(AccessControlFilter.java:133) ~[shiro-web-1.4.2.jar:1.4.2]
at org.apache.shiro.web.filter.AccessControlFilter.onPreHandle(AccessControlFilter.java:162) ~[shiro-web-1.4.2.jar:1.4.2]
at org.apache.shiro.web.filter.PathMatchingFilter.isFilterChainContinued(PathMatchingFilter.java:203) ~[shiro-web-1.4.2.jar:1.4.2]
at org.apache.shiro.web.filter.PathMatchingFilter.preHandle(PathMatchingFilter.java:178) ~[shiro-web-1.4.2.jar:1.4.2]
at org.apache.shiro.web.servlet.AdviceFilter.doFilterInternal(AdviceFilter.java:131) ~[shiro-web-1.4.2.jar:1.4.2]
at org.apache.shiro.web.servlet.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:125) ~[shiro-web-1.4.2.jar:1.4.2]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-9.0.36.jar:9.0.36]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.36.jar:9.0.36]
at com.alibaba.druid.support.http.WebStatFilter.doFilter(WebStatFilter.java:124) ~[druid-1.1.21.jar:1.1.21]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-9.0.36.jar:9.0.36]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.36.jar:9.0.36]
at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[spring-web-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-9.0.36.jar:9.0.36]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.36.jar:9.0.36]
at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[spring-web-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-9.0.36.jar:9.0.36]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.36.jar:9.0.36]
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) ~[spring-web-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-9.0.36.jar:9.0.36]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.36.jar:9.0.36]
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202) ~[tomcat-embed-core-9.0.36.jar:9.0.36]
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96) [tomcat-embed-core-9.0.36.jar:9.0.36]
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:541) [tomcat-embed-core-9.0.36.jar:9.0.36]
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:139) [tomcat-embed-core-9.0.36.jar:9.0.36]
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92) [tomcat-embed-core-9.0.36.jar:9.0.36]
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74) [tomcat-embed-core-9.0.36.jar:9.0.36]
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343) [tomcat-embed-core-9.0.36.jar:9.0.36]
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:373) [tomcat-embed-core-9.0.36.jar:9.0.36]
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65) [tomcat-embed-core-9.0.36.jar:9.0.36]
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:868) [tomcat-embed-core-9.0.36.jar:9.0.36]
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1590) [tomcat-embed-core-9.0.36.jar:9.0.36]
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) [tomcat-embed-core-9.0.36.jar:9.0.36]
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) [na:1.8.0_241]
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) [na:1.8.0_241]
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) [tomcat-embed-core-9.0.36.jar:9.0.36]
at java.lang.Thread.run(Thread.java:748) [na:1.8.0_241]
后来跟了一下代码,在AccessControlFilter类中SecurityUtils.getSubject()方法,进入后,在下面这段代码处:
public static Subject getSubject() {
Subject subject = ThreadContext.getSubject();
if (subject == null) {
subject = (new Builder()).buildSubject();
ThreadContext.bind(subject);
}
return subject;
}
获取的subject 一直为null,导致后续异常抛出。
解决办法:
改一下上面的ShiroConfig类中KickoutSessionControlFilter的bean代码顺序即可,将kickoutSessionControlFilter的代码,移到ShiroFilterFactoryBean的bean代码后面就行了。
如下:
//注入工厂
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager securityManager
,@Qualifier("kickoutSessionControlFilter") KickoutSessionControlFilter kickoutSessionControlFilter){
ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
factoryBean.setSecurityManager(securityManager);
//自定义拦截器限制并发人数,参考博客
LinkedHashMap<String, Filter> filtersMap = new LinkedHashMap<>();
//限制同一帐号同时在线的个数
filtersMap.put("kickout", kickoutSessionControlFilter);
factoryBean.setFilters(filtersMap);
//权限设置
Map<String,String> map = new LinkedHashMap<>();
map.put("/admin","anon");
map.put("/admin/login.do","anon");
map.put("/payRecord/pay.do","anon");
map.put("/admin/index.do","kickout,authc");
map.put("/view/list.do","kickout,authc");
map.put("/line/list.do","kickout,authc");
map.put("/order/manageList.do","kickout,authc");
map.put("/user/private/**","kickout,authc");
map.put("/leaveMessage/**","kickout,authc");
map.put("/payRecord/toPay.do","kickout,authc");
map.put("/order/**","kickout,authc");
map.put("/**","kickout");
map.put("/manage","perms[manage]");
map.put("/administrator","roles[administrator]");
factoryBean.setFilterChainDefinitionMap(map);
//设置登陆页面
factoryBean.setLoginUrl("/f_login.html");
//设置未授权页面
factoryBean.setUnauthorizedUrl("/unauth");
return factoryBean;
}
/**
* 并发登录控制
* @return
*/
@Bean
public KickoutSessionControlFilter kickoutSessionControlFilter(@Qualifier("sessionManager") DefaultWebSessionManager sessionManager,
@Qualifier("redisCacheManager") com.yangy.web.realm.RedisCacheManager redisCacheManager){
KickoutSessionControlFilter kickoutSessionControlFilter = new KickoutSessionControlFilter();
//用于根据会话ID,获取会话进行踢出操作的;
kickoutSessionControlFilter.setSessionManager(sessionManager);
//使用cacheManager获取相应的cache来缓存用户登录的会话;用于保存用户—会话之间的关系的;
kickoutSessionControlFilter.setCacheManager(redisCacheManager);
//是否踢出后来登录的,默认是false;即后者登录的用户踢出前者登录的用户;
kickoutSessionControlFilter.setKickoutAfter(false);
//同一个用户最大的会话数,默认1;比如2的意思是同一个用户允许最多同时两个人登录;
kickoutSessionControlFilter.setMaxSession(1);
//被踢出后重定向到的地址;
kickoutSessionControlFilter.setKickoutUrl("/user/login.do?kickout=1");
return kickoutSessionControlFilter;
}
2.3.2 KickoutSessionControlFilter并发在线人数控制失效
请求成功之后,但是我在切换浏览器,使用同一账户登录之后,却没有被挤下线!KickoutSessionControlFilter的并发在线人数控制没有生效,不起作用了。。无语了~~~
开始找寻问题所在了。进入KickoutSessionControlFilter的代码,根据代码的含义,基本就是将同一账户的名称作为key,存储一个LinkedList到缓存中。并通过我们配置的最大并发数数量,来控制LinkedList中的并发数量。但是,通过断点进入发现,在下面这段代码处,出了问题:
// 同步控制
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);
}
这段代码中的deque每次取出来时,里面都只是一个空队列。由于这个过滤器中的CacheManager是自定义的,我是结合redis整合的缓存管理。所以,从缓存中取出来的数据有问题,我第一时间怀疑是否是CacheManager出了问题,因为网上其他帖子里大部分都是整合的ehcache,于是开始切换缓存管理的自定义代码,分别如下:
第一种:使用项目内整合的RedisTemplate作为shiro缓存管理
代码如下:
package com.yangy.web.realm;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.data.redis.core.RedisTemplate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* @Author: Yangy
* @Date: 2021/8/30 16:51
* @Description
*/
public class RedisCache<K, V> implements Cache<K, V> {
private long cacheLive;
private String cacheKeyPrefix;
private RedisTemplate redisTemplate;
@Override
public V get(K k) throws CacheException {
return (V) this.redisTemplate.opsForValue().get(this.getRedisCacheKey(k));
}
@Override
public V put(K k, V v) throws CacheException {
redisTemplate.opsForValue().set(this.getRedisCacheKey(k), v, cacheLive, TimeUnit.MINUTES);
return v;
}
@Override
public V remove(K k) throws CacheException {
V obj = (V) this.redisTemplate.opsForValue().get(this.getRedisCacheKey(k));
redisTemplate.delete(this.getRedisCacheKey(k));
return obj;
}
@Override
public void clear() throws CacheException {
Set keys = this.redisTemplate.keys(this.cacheKeyPrefix + "*");
if (null != keys && keys.size() > 0) {
Iterator itera = keys.iterator();
this.redisTemplate.delete(itera.next());
}
}
@Override
public int size() {
Set<K> keys = this.redisTemplate.keys(this.cacheKeyPrefix + "*");
return keys.size();
}
@Override
public Set<K> keys() {
return this.redisTemplate.keys(this.cacheKeyPrefix + "*");
}
@Override
public Collection<V> values() {
Set<K> keys = this.redisTemplate.keys(this.cacheKeyPrefix + "*");
Set<V> values = new HashSet<V>(keys.size());
for (K key : keys) {
values.add((V) this.redisTemplate.opsForValue().get(this.getRedisCacheKey(key)));
}
return values;
}
private String getRedisCacheKey(K key) {
Object redisKey = this.getStringRedisKey(key);
if (redisKey instanceof String) {
return this.cacheKeyPrefix + redisKey;
} else {
return String.valueOf(redisKey);
}
}
private Object getStringRedisKey(K key) {
Object redisKey;
if (key instanceof PrincipalCollection) {
redisKey = this.getRedisKeyFromPrincipalCollection((PrincipalCollection) key);
} else {
redisKey = key.toString();
}
return redisKey;
}
private Object getRedisKeyFromPrincipalCollection(PrincipalCollection key) {
List realmNames = this.getRealmNames(key);
Collections.sort(realmNames);
Object redisKey = this.joinRealmNames(realmNames);
return redisKey;
}
private List<String> getRealmNames(PrincipalCollection key) {
ArrayList realmArr = new ArrayList();
Set realmNames = key.getRealmNames();
Iterator i$ = realmNames.iterator();
while (i$.hasNext()) {
String realmName = (String) i$.next();
realmArr.add(realmName);
}
return realmArr;
}
private Object joinRealmNames(List<String> realmArr) {
StringBuilder redisKeyBuilder = new StringBuilder();
for (int i = 0; i < realmArr.size(); ++i) {
String s = realmArr.get(i);
redisKeyBuilder.append(s);
}
String redisKey = redisKeyBuilder.toString();
return redisKey;
}
public RedisCache(RedisTemplate redisTemplate, long cacheLive, String cachePrefix) {
this.redisTemplate = redisTemplate;
this.cacheLive = cacheLive;
this.cacheKeyPrefix = cachePrefix;
}
}
package com.yangy.web.realm;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.apache.shiro.cache.CacheManager;
import org.springframework.data.redis.core.RedisTemplate;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
/**
* @Author: Yangy
* @Date: 2021/8/30 16:50
* @Description
*/
public class RedisCacheManager implements CacheManager {
private long cacheLive; //cache存活时间
private String cacheKeyPrefix; //cache前缀
private RedisTemplate redisTemplate;
private final ConcurrentMap<String, Cache> caches = new ConcurrentHashMap<String, Cache>();
@Override
public <K, V> Cache<K, V> getCache(String name) throws CacheException {
Cache cache = this.caches.get(name);
if (cache == null) {
//自定义shiroCache
cache = new RedisCache(redisTemplate, cacheLive, cacheKeyPrefix);
this.caches.put(name, cache);
}
return cache;
}
public void setCacheLive(long cacheLive) {
this.cacheLive = cacheLive;
}
public void setCacheKeyPrefix(String cacheKeyPrefix) {
this.cacheKeyPrefix = cacheKeyPrefix;
}
public void setRedisTemplate(RedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
}
shiroConfig中引入上述配置即可
@Bean
public com.yangy.web.realm.RedisCacheManager redisCacheManager(@Qualifier("redisTemplate") RedisTemplate redisTemplate){
com.yangy.web.realm.RedisCacheManager redisCacheManager = new com.yangy.web.realm.RedisCacheManager();
redisCacheManager.setCacheLive(120);
redisCacheManager.setRedisTemplate(redisTemplate);
return redisCacheManager;
}
第二种:使用插件jar包shiro-redis作为shiro缓存管理
这种方式需要导入依赖包,如下:
<!-- shiro+redis缓存插件 -->
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>3.1.0</version>
</dependency>
shiroConfig中引入:
@Bean
public RedisManager getRedisManager(){
RedisManager redisManager = new RedisManager();
redisManager.setHost("172.16.50.100");
redisManager.setPort(6379);
redisManager.setDatabase(1);
return redisManager;
}
@Bean
public RedisCacheManager getRedisCacheManager(){
RedisCacheManager redisCacheManager = new RedisCacheManager();
redisCacheManager.setRedisManager(getRedisManager());
redisCacheManager.setExpire(120);
return redisCacheManager;
}
然而,切换尝试之后,发现还是不行。
由此可见,并不是缓存管理所导致。开始其他的方向排查,此间,参考过张开涛大神的git源码(git地址:https://github.com/zhangkaitao/shiro-example),在shiro-example-chapter18中查看到KickoutSessionControlFilter类的代码,和上述几乎一致。也参考了多篇网上的帖子,发现大家几乎都是类似的代码。
这时就有点怀疑这代码了,是不是有啥子问题。为啥没起作用呢????(网上的代码特么的都是骗人的吧,坑爹··········麻了~~~~)
于是,继续检查KickoutSessionControlFilter类中的代码,发现此处有大问题:
//如果队列里没有此sessionId,且用户没有被踢出;放入队列
if(!deque.contains(sessionId) && session.getAttribute("kickout") == null) {
deque.push(sessionId);
}
这里代码是将没有被踢出的账户的sessionId放入LinkedList中,但是,我们每次处理的时候,都是从缓存当中取出同一账户的LinkedList来做处理的。此时,问题就出在了没有将LinkedList刷新到缓存中,所以每次从缓存中取出LinkedList时,都会是第一次放进去的空的LinkedList。
解决办法:
将代码改成如下:
//如果队列里没有此sessionId,且用户没有被踢出;放入队列 if(!deque.contains(sessionId) && session.getAttribute("kickout") == null) { deque.push(sessionId); cache.put(username, deque); }
再次尝试,顺利控制同时在线的并发人数,后登录的账户,会把前面登录的账户sessionId剔除掉,操作时就会被提示,已被挤下线啦。。。。我这边的效果如下:
再贴一下前端页面提示框的代码吧,如下:
<script>
var href=location.href;
if(href.indexOf("kickout")>0){
layer.msg("您的账号在另一台设备上登录,如非本人操作,请立即修改密码!",{offset: '100px'});
layer.open({
type: 0,
title :"下线提示",
closeBtn: 1,
anim: 6,
content: '您的账号在另一台设备上登录,如非本人操作,请立即修改密码!'
});
}
</script>
3、总结
问题终于解决啦,耗费了不少时间。总结出来,分享一下!
最后想告诫我们大家,解决问题时要静下心来,对于网上的解决办法以及代码。也要有自己的理解。。照搬的话,就会出现问题而无法解决。。
PS:我这里有个疑问,就是张开涛大神的代码类KickoutSessionControlFilter那里是没有问题吗?为啥我使用的时候就不起作用呢?有知道的朋友,欢迎下方留言探讨!!!
分享一下shiro的中文API教程网址:https://www.w3cschool.cn/shiro/andc1if0.html
以上是关于全栈编程系列SpringBoot整合Shiro(含KickoutSessionControlFilter并发在线人数控制以及不生效问题配置启动异常No SecurityManager...)(代码片段的主要内容,如果未能解决你的问题,请参考以下文章
SpringBoot系列十二:SpringBoot整合 Shiro
SpringBoot技术专题「实战开发系列」带你一同探索Shiro整合JWT授权和认证实战开发