全栈编程系列SpringBoot整合Shiro(含KickoutSessionControlFilter并发在线人数控制以及不生效问题配置启动异常No SecurityManager...)(代码片段

Posted 善良勤劳勇敢而又聪明的老杨

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了全栈编程系列SpringBoot整合Shiro(含KickoutSessionControlFilter并发在线人数控制以及不生效问题配置启动异常No SecurityManager...)(代码片段相关的知识,希望对你有一定的参考价值。

热门系列:


目录

1、前言

2、正文

2.1 环境介绍

2.2 shiro配置

2.3 shiro并发在线人数控制KickoutSessionControlFilter

2.3.1 自定义shiro的KickoutSessionControlFilter请求时报错异常

2.3.2 KickoutSessionControlFilter并发在线人数控制失效

3、总结


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

        其实今天,主要想记录的是这一块。因为这一块整合的时候,遇到不少问题,耗费了不少时间。下面逐一讲一下,主要有两个问题:

  1. 整合自定义KickoutSessionControlFilter时请求发生异常
  2. 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授权和认证实战开发

SpringBoot整合shiro系列-SpingBoot是如何将shiroFilter注册到servlet容器中的

SpringBoot整合Shiro安全框架

SpringBoot进阶之整合Shiro鉴权框架(三)

Springboot整合Shiro