springboot+shiro控制同一用户在线的并发数,同一个用户最大的会话数,默认1;比如2的意思是同一个用户允许最多同时两个人登录

文章介绍了如何在SpringBoot项目中使用Shiro框架进行用户认证和权限管理,并特别关注了如何配置和实现限制同一用户并发登录的数量,包括创建ShiroFilterFactoryBean、自定义Realm、配置SessionManager、设置踢出策略等步骤。
部署运行你感兴趣的模型镜像

springboot+shiro控制同一用户在线的并发数,同一个用户最大的会话数,默认1;比如2的意思是同一个用户允许最多同时两个人登录
具体实现类粘贴到了下方,有需要的直接复制使用即可

ShiroConfig 配置类

package com.XXX.web.config.auth;

import com.XXX.web.login.UserRealm;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.cache.ehcache.EhCacheManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.util.ThreadContext;
import org.apache.shiro.web.filter.authc.LogoutFilter;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
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 org.springframework.context.annotation.DependsOn;
import org.springframework.http.HttpStatus;

import javax.servlet.Filter;
import java.util.LinkedHashMap;
import java.util.Map;

@Configuration
public class ShiroConfig {

    /**
     * LifecycleBeanPostProcessor,这是个DestructionAwareBeanPostProcessor的子类,
     * 负责org.apache.shiro.util.Initializable类型bean的生命周期的,初始化和销毁。
     * 主要是AuthorizingRealm类的子类,以及EhCacheManager类。
     */
    @Bean(name = "lifecycleBeanPostProcessor")
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

    /**
     * HashedCredentialsMatcher,这个类是为了对密码进行编码的,
     * 防止密码在数据库里明码保存,当然在登陆认证的时候,
     * 这个类也负责对form里输入的密码进行编码。
     */
    @Bean(name = "hashedCredentialsMatcher")
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
        credentialsMatcher.setHashAlgorithmName("MD5");
        credentialsMatcher.setHashIterations(2);
        credentialsMatcher.setStoredCredentialsHexEncoded(true);
        return credentialsMatcher;
    }

    /**
     * UserRealm,这是个自定义的认证类,继承自AuthorizingRealm,
     * 负责用户的认证和权限的处理,可以参考JdbcRealm的实现。
     */
    @Bean(name = "userRealm")
    @DependsOn("lifecycleBeanPostProcessor")
    public UserRealm userRealm() {
        UserRealm realm = new UserRealm();
        realm.setCredentialsMatcher(hashedCredentialsMatcher());
        return realm;
    }

    /**
     * 设置session失效时间
     *
     * @return
     */
    @Bean(name = "sessionManager")
    public DefaultWebSessionManager sessionManager() {
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        //设置session过期时间3600s
        sessionManager.setGlobalSessionTimeout(3600000L);
        return sessionManager;
    }

    /**
     * 自定义缓存管理器
     *
     * @return
     */
    @Bean(name = "ehCacheManager")
    public EhCacheManager ehCacheManager() {
        EhCacheManager cacheManager = new EhCacheManager();
        return cacheManager;
    }

    /**
     * SecurityManager,权限管理,这个类组合了登陆,登出,权限,session的处理,是个比较重要的类。
     */
    @Bean(name = "securityManager")
    public DefaultWebSecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(userRealm());
        securityManager.setSessionManager(sessionManager());
        securityManager.setCacheManager(ehCacheManager());
        ThreadContext.bind(securityManager);
        return securityManager;
    }

    /**
     * ShiroFilterFactoryBean,是个factorybean,为了生成ShiroFilter。
     * 它主要保持了三项数据,securityManager,filters,filterChainDefinitionManager。
     */
    @Bean(name = "shiroFilter")
    public ShiroFilterFactoryBean shiroFilterFactoryBean() {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        //Shiro的核心安全接口,这个属性是必须的
        shiroFilterFactoryBean.setSecurityManager(securityManager());

        //Shiro登出后转发到登录页面
        Map<String, Filter> filtersDefinition = new LinkedHashMap<>();
        LogoutFilter logoutFilter = new LogoutFilter();
        logoutFilter.setRedirectUrl("/login");
        filtersDefinition.put("logout", logoutFilter);
        filtersDefinition.put("authc", new ShiroLoginFilter(HttpStatus.UNAUTHORIZED.value())); //自定义状态码
        filtersDefinition.put("kickout", kickoutSessionControlFilter());  // 限制同一帐号同时在线的个数
        shiroFilterFactoryBean.setFilters(filtersDefinition);
        /**
         * Shiro内置过滤器,可以实现权限相关的拦截器
         *    常用的过滤器:
         *       anon: 无需认证(登录)可以访问
         *       authc: 必须认证才可以访问
         *       user: 如果使用rememberMe的功能可以直接访问
         *       perms: 该资源必须得到资源权限才可以访问
         *       roles: 该资源必须得到角色权限才可以访问
         */
        Map<String, String> filterChainDefinitionManager = new LinkedHashMap<>();
        filterChainDefinitionManager.put("/logout", "logout");
        filterChainDefinitionManager.put("/XXX/api/**", "kickout,authc");
        filterChainDefinitionManager.put("/XXX/**", "anon");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionManager);
        return shiroFilterFactoryBean;
    }

    /**
     * 并发登录控制
     *
     * @return
     */
    @Bean(name = "kickoutSessionControlFilter")
    public KickoutSessionFilter kickoutSessionControlFilter() {
        KickoutSessionFilter kickoutSessionFilter = new KickoutSessionFilter();
        // 用于根据会话ID,获取会话进行踢出操作的;
        kickoutSessionFilter.setSessionManager(sessionManager());
        // 使用cacheManager获取相应的cache来缓存用户登录的会话;用于保存用户—会话之间的关系的;
        kickoutSessionFilter.setCacheManager(ehCacheManager());
        // 是否踢出后来登录的,默认是false;即后者登录的用户踢出前者登录的用户;
        kickoutSessionFilter.setKickoutAfter(false);
        // 同一个用户最大的会话数,默认1;比如2的意思是同一个用户允许最多同时两个人登录;
        kickoutSessionFilter.setMaxSession(1);
        // 被踢出后重定向到的地址;
        kickoutSessionFilter.setKickoutUrl("/login?kickout=1");
        return kickoutSessionFilter;
    }

    /**
     * DefaultAdvisorAutoProxyCreator,Spring的一个bean,由Advisor决定对哪些类的方法进行AOP代理。
     */
    @Bean
    @ConditionalOnMissingBean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAAP = new DefaultAdvisorAutoProxyCreator();
        defaultAAP.setProxyTargetClass(true);
        return defaultAAP;
    }

    /**
     * AuthorizationAttributeSourceAdvisor,shiro里实现的Advisor类,
     * 内部使用Aop AllianceAnnotationsAuthorizing MethodInterceptor来拦截用以下注解的方法。
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
        AuthorizationAttributeSourceAdvisor aASA = new AuthorizationAttributeSourceAdvisor();
        aASA.setSecurityManager(securityManager());
        return aASA;
    }

}

UserRealm 类

package com.XXX.web.login;

import com.XXX.web.entity.user.UserEntity;
import com.XXX.web.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;

import javax.annotation.Resource;

@Slf4j
public class UserRealm extends AuthorizingRealm {

    @Resource
    private UserService userService;

    //授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        log.info("执行了=>授权doGetAuthorizationInfo");
        return null;
    }

    //认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        log.info("执行了=>认证doGetAuthenticationInfo");
        UsernamePasswordToken userToken = (UsernamePasswordToken) token;
        UserEntity user = userService.queryUserByName(userToken.getUsername());  //认证用户名和密码 数据库中取
        //此时return null就会抛出异常 UnknownAccountException
        if (user == null) {return null;}
        /**
         * 验证密码,我们可以使用一个AuthenticationInfo实现类 SimpleAuthenticationInfo
         * 密码认证,shiro帮你做,只需要像下面把正确的密码丢进去
         * shiro会自动帮我们验证!重点是第二个参数就是要验证的密码!
         */
        return new SimpleAuthenticationInfo(user.getUsername(), user.getPassword(), ByteSource.Util.bytes(user.getSalt()), getName());
    }
}

ShiroLoginFilter 类,登录拦截器

package com.XXX.web.config.auth;

import com.alibaba.fastjson.JSON;
import com.XXX.web.common.Response;
import org.apache.shiro.web.filter.authc.FormAuthenticationFilter;
import org.springframework.http.MediaType;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class ShiroLoginFilter extends FormAuthenticationFilter {
    private Integer code;

    public ShiroLoginFilter(Integer code) {
        this.code = code;
    }

    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException {
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        httpServletResponse.setCharacterEncoding("UTF-8");
        httpServletResponse.setContentType(MediaType.APPLICATION_JSON_VALUE);
        httpServletResponse.setStatus(code);
        httpServletResponse.getWriter().write(JSON.toJSONString(Response.of("User not logged in", code, "用户未登录,请先进行身份验证!")));
        return false;
    }

}

踢出用户类

import com.alibaba.fastjson.JSON;
import org.apache.shiro.SecurityUtils;
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.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import java.io.Serializable;
import java.util.Deque;
import java.util.LinkedList;


@Configuration
public class KickoutSessionFilter extends AccessControlFilter {

    /*前端需要加的代码
     <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>
     */


    /**
     * 踢出后到的地址
     */
    private String kickoutUrl;

    /**
     * 踢出之前登录的/之后登录的用户 默认踢出之前登录的用户
     */
    private boolean kickoutAfter = false;

    /**
     * 同一个帐号最大会话数 默认1
     */
    private int maxSession = 1;

    /**
     * session管理器
     */
    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");
    }

    /**
     * 是否允许访问
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        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();
        String username = (String) SecurityUtils.getSubject().getPrincipal();
        Serializable sessionId = session.getId();

        // 同步控制
        Deque<Serializable> deque = cache.get(username);
        if (deque == null) {
            deque = new LinkedList<>();
            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) {
                e.printStackTrace();
            }
        }

        // 如果被踢出了,直接退出,重定向到踢出后的地址
        if (session.getAttribute("kickout") != null) {
            // 会话被踢出了
            try {
                subject.logout();
            } catch (Exception e) {
                e.printStackTrace();
            }
            saveRequest(request);

            HttpServletRequest httpRequest = WebUtils.toHttp(request);
            HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
            // 如果是ajax请求
            if (isAjax(httpRequest)) {
                // 使得http会话过期
                httpServletResponse.sendError(0);
            } else {
//                httpServletResponse.setCharacterEncoding("UTF-8");
//                httpServletResponse.setContentType(MediaType.APPLICATION_JSON_VALUE);
//                httpServletResponse.setStatus(HttpStatus.UNAUTHORIZED.value());
//                httpServletResponse.getWriter().write(JSON.toJSONString(Response.of("The user is already logged in on another device", HttpStatus.UNAUTHORIZED.value(), "您的账号在另一台设备上登录,如非本人操作,请立即修改密码!")));
                WebUtils.issueRedirect(request, response, kickoutUrl);
            }
            return false;
        }
        return true;
    }

    /**
     * 判断是否为ajax请求
     *
     * @param request
     * @return boolean对象
     */
    public static boolean isAjax(ServletRequest request) {
        return "XMLHttpRequest".equalsIgnoreCase(((HttpServletRequest) request).getHeader("X-Requested-With"));
    }

}

您可能感兴趣的与本文相关的镜像

Llama Factory

Llama Factory

模型微调
LLama-Factory

LLaMA Factory 是一个简单易用且高效的大型语言模型(Large Language Model)训练与微调平台。通过 LLaMA Factory,可以在无需编写任何代码的前提下,在本地完成上百种预训练模型的微调

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

柒嘴八舌

你的鼓励将是我创造最大的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值